Skip to content

Transpilation

Contains functions that operate on blocks and circuits to transpile them to new blocks/circuits.

transpile(*fs)

AbstractBlock or QuantumCircuit transpilation. Compose functions that accept a circuit/block and returns a circuit/block.

PARAMETER DESCRIPTION
*fs

composable functions that either map blocks to blocks (Callable[[AbstractBlock], AbstractBlock]) or circuits to circuits (Callable[[QuantumCircuit], QuantumCircuit]).

TYPE: Callable DEFAULT: ()

RETURNS DESCRIPTION
Callable

Composed function.

Examples:

Flatten a block of nested chains and krons:

from qadence import *
from qadence.transpile import transpile, flatten, scale_primitive_blocks_only

b = chain(2 * chain(chain(X(0), Y(0))), kron(kron(X(0), X(1))))
print(b)

# both flatten and scale_primitive_blocks_only are functions that accept and
# return a block
t = transpile(flatten, scale_primitive_blocks_only)(b)
print(t)
ChainBlock(0,1)
├── [mul: 2] 
   └── ChainBlock(0)
       └── ChainBlock(0)
           ├── X(0)
           └── Y(0)
└── KronBlock(0,1)
    └── KronBlock(0,1)
        ├── X(0)
        └── X(1)

ChainBlock(0,1)
├── [mul: 2.00000000000000] 
   └── X(0)
├── Y(0)
└── KronBlock(0,1)
    ├── X(0)
    └── X(1)

We also proved a decorator to easily turn a function Callable[[AbstractBlock], AbstractBlock] into a Callable[[QuantumCircuit], QuantumCircuit] to be used in circuit transpilation.

from qadence import *
from qadence.transpile import transpile, blockfn_to_circfn, flatten

# We want to pass this circuit to `transpile` instead of a block,
# so we need functions that map from a circuit to a circuit.
circ = QuantumCircuit(2, chain(chain(X(0), chain(X(1)))))

@blockfn_to_circfn
def fn(block):
    # un-decorated function accepts a block and returns a block
    return block * block

transp = transpile(
    # the decorated function accepts a circuit and returns a circuit
    fn,
    # already existing functions can also be decorated
    blockfn_to_circfn(flatten)
)
print(transp(circ))
ChainBlock(0,1)
├── ChainBlock(0,1)
   ├── X(0)
   └── X(1)
└── ChainBlock(0,1)
    ├── X(0)
    └── X(1)

Source code in qadence/transpile/transpile.py
def transpile(*fs: Callable) -> Callable:
    """`AbstractBlock` or `QuantumCircuit` transpilation. Compose functions that
    accept a circuit/block and returns a circuit/block.

    Arguments:
        *fs: composable functions that either map blocks to blocks
            (`Callable[[AbstractBlock], AbstractBlock]`)
            or circuits to circuits (`Callable[[QuantumCircuit], QuantumCircuit]`).

    Returns:
        Composed function.


    Examples:

    Flatten a block of nested chains and krons:
    ```python exec="on" source="material-block" result="json"
    from qadence import *
    from qadence.transpile import transpile, flatten, scale_primitive_blocks_only

    b = chain(2 * chain(chain(X(0), Y(0))), kron(kron(X(0), X(1))))
    print(b)
    print() # markdown-exec: hide

    # both flatten and scale_primitive_blocks_only are functions that accept and
    # return a block
    t = transpile(flatten, scale_primitive_blocks_only)(b)
    print(t)
    ```

    We also proved a decorator to easily turn a function `Callable[[AbstractBlock], AbstractBlock]`
    into a `Callable[[QuantumCircuit], QuantumCircuit]` to be used in circuit transpilation.
    ```python exec="on" source="material-block" result="json"
    from qadence import *
    from qadence.transpile import transpile, blockfn_to_circfn, flatten

    # We want to pass this circuit to `transpile` instead of a block,
    # so we need functions that map from a circuit to a circuit.
    circ = QuantumCircuit(2, chain(chain(X(0), chain(X(1)))))

    @blockfn_to_circfn
    def fn(block):
        # un-decorated function accepts a block and returns a block
        return block * block

    transp = transpile(
        # the decorated function accepts a circuit and returns a circuit
        fn,
        # already existing functions can also be decorated
        blockfn_to_circfn(flatten)
    )
    print(transp(circ))
    ```
    """
    return lambda x: reduce(lambda acc, f: f(acc), reversed(fs), x)

chain_single_qubit_ops(block)

Transpile a chain of krons into a kron of chains of single qubit operations.

Examples:

from qadence import hea
from qadence.transpile.block import chain_single_qubit_ops

# Consider a single HEA layer
block = hea(2,1)
print(block)

# After applying chain_single_qubit_ops, we get:
print(chain_single_qubit_ops(block))
ChainBlock(0,1) [tag: HEA]
├── ChainBlock(0,1)
   ├── KronBlock(0,1)
      ├── RX(0) [params: ['theta_0']]
      └── RX(1) [params: ['theta_1']]
   ├── KronBlock(0,1)
      ├── RY(0) [params: ['theta_2']]
      └── RY(1) [params: ['theta_3']]
   └── KronBlock(0,1)
       ├── RX(0) [params: ['theta_4']]
       └── RX(1) [params: ['theta_5']]
└── ChainBlock(0,1)
    └── KronBlock(0,1)
        └── CNOT(0,1)
ChainBlock(0,1)
├── KronBlock(0,1)
   ├── ChainBlock(0)
      ├── RX(0) [params: ['theta_0']]
      ├── RY(0) [params: ['theta_2']]
      └── RX(0) [params: ['theta_4']]
   └── ChainBlock(1)
       ├── RX(1) [params: ['theta_1']]
       ├── RY(1) [params: ['theta_3']]
       └── RX(1) [params: ['theta_5']]
└── ChainBlock(0,1)
    └── KronBlock(0,1)
        └── CNOT(0,1)

Source code in qadence/transpile/block.py
def chain_single_qubit_ops(block: AbstractBlock) -> AbstractBlock:
    """Transpile a chain of krons into a kron of chains of single qubit operations.

    Examples:
    ```python exec="on" source="above" result="json"
    from qadence import hea
    from qadence.transpile.block import chain_single_qubit_ops

    # Consider a single HEA layer
    block = hea(2,1)
    print(block)

    # After applying chain_single_qubit_ops, we get:
    print(chain_single_qubit_ops(block))
    ```
    """
    if is_chain_of_primitivekrons(block):
        try:
            return kron(*map(lambda bs: chain(*bs), zip(*block)))  # type: ignore[misc]
        except Exception as e:
            logger.debug(
                f"Unable to transpile {block} using chain_single_qubit_ops\
                         due to {e}. Returning original circuit."
            )
            return block

    elif isinstance(block, CompositeBlock):
        return _construct(type(block), tuple(chain_single_qubit_ops(b) for b in block.blocks))
    else:
        return block

flatten(block, types=[ChainBlock, KronBlock, AddBlock])

Flattens the given types of CompositeBlocks if possible.

Example:

from qadence import chain, kron, X
from qadence.transpile import flatten
from qadence.blocks import ChainBlock, KronBlock, AddBlock

x = chain(chain(chain(X(0))), kron(kron(X(0))))

# flatten only `ChainBlock`s
assert flatten(x, [ChainBlock]) == chain(X(0), kron(kron(X(0))))

# flatten `ChainBlock`s and `KronBlock`s
assert flatten(x, [ChainBlock, KronBlock]) == chain(X(0), kron(X(0)))

# flatten `AddBlock`s (does nothing in this case)
assert flatten(x, [AddBlock]) == x

Source code in qadence/transpile/block.py
def flatten(block: AbstractBlock, types: list = [ChainBlock, KronBlock, AddBlock]) -> AbstractBlock:
    """Flattens the given types of `CompositeBlock`s if possible.

    Example:
    ```python exec="on" source="material-block" result="json"
    from qadence import chain, kron, X
    from qadence.transpile import flatten
    from qadence.blocks import ChainBlock, KronBlock, AddBlock

    x = chain(chain(chain(X(0))), kron(kron(X(0))))

    # flatten only `ChainBlock`s
    assert flatten(x, [ChainBlock]) == chain(X(0), kron(kron(X(0))))

    # flatten `ChainBlock`s and `KronBlock`s
    assert flatten(x, [ChainBlock, KronBlock]) == chain(X(0), kron(X(0)))

    # flatten `AddBlock`s (does nothing in this case)
    assert flatten(x, [AddBlock]) == x
    ```
    """
    if isinstance(block, CompositeBlock):

        def fn(b: AbstractBlock, T: Type) -> AbstractBlock:
            return _construct(type(block), tuple(_flat_blocks(b, T)))

        return reduce(fn, types, block)  # type: ignore[arg-type]
    elif isinstance(block, ScaleBlock):
        blk = deepcopy(block)
        blk.block = flatten(block.block, types=types)
        return blk
    else:
        return block

scale_primitive_blocks_only(block, scale=None)

When given a scaled CompositeBlock consisting of several PrimitiveBlocks, move the scale all the way down into the leaves of the block tree.

PARAMETER DESCRIPTION
block

The block to be transpiled.

TYPE: AbstractBlock

scale

An optional scale parameter. Only to be used for recursive calls internally.

TYPE: Basic DEFAULT: None

RETURNS DESCRIPTION
AbstractBlock

A block of the same type where the scales have been moved into the subblocks.

TYPE: AbstractBlock

Examples:

There are two different cases: ChainBlocks/KronBlocks: Only the first subblock needs to be scaled because chains/krons represent multiplications.

from qadence import chain, X, RX
from qadence.transpile import scale_primitive_blocks_only
b = 2 * chain(X(0), RX(0, "theta"))
print(b)
# After applying scale_primitive_blocks_only
print(scale_primitive_blocks_only(b))
[mul: 2] 
└── ChainBlock(0)
    ├── X(0)
    └── RX(0) [params: ['theta']]
ChainBlock(0)
├── [mul: 2.00000000000000] 
   └── X(0)
└── RX(0) [params: ['theta']]

AddBlocks: Consider 2 * add(X(0), RX(0, "theta")). The scale needs to be added to all subblocks. We get add(2 * X(0), 2 * RX(0, "theta")).

from qadence import add, X, RX
from qadence.transpile import scale_primitive_blocks_only
b = 2 * add(X(0), RX(0, "theta"))
print(b)
# After applying scale_primitive_blocks_only
print(scale_primitive_blocks_only(b))
[mul: 2] 
└── AddBlock(0)
    ├── X(0)
    └── RX(0) [params: ['theta']]
AddBlock(0)
├── [mul: 2.00000000000000] 
   └── X(0)
└── [mul: 2.00000000000000] 
    └── RX(0) [params: ['theta']]

Source code in qadence/transpile/block.py
@singledispatch
def scale_primitive_blocks_only(block: AbstractBlock, scale: sympy.Basic = None) -> AbstractBlock:
    """When given a scaled CompositeBlock consisting of several PrimitiveBlocks,
    move the scale all the way down into the leaves of the block tree.

    Arguments:
        block: The block to be transpiled.
        scale: An optional scale parameter. Only to be used for recursive calls internally.

    Returns:
        AbstractBlock: A block of the same type where the scales have been moved into the subblocks.

    Examples:

    There are two different cases:
    `ChainBlock`s/`KronBlock`s: Only the first subblock needs to be scaled because chains/krons
    represent multiplications.
    ```python exec="on" source="above" result="json"
    from qadence import chain, X, RX
    from qadence.transpile import scale_primitive_blocks_only
    b = 2 * chain(X(0), RX(0, "theta"))
    print(b)
    # After applying scale_primitive_blocks_only
    print(scale_primitive_blocks_only(b))
    ```

    `AddBlock`s: Consider 2 * add(X(0), RX(0, "theta")).  The scale needs to be added to all
    subblocks.  We get add(2 * X(0), 2 * RX(0, "theta")).
    ```python exec="on" source="above" result="json"
    from qadence import add, X, RX
    from qadence.transpile import scale_primitive_blocks_only
    b = 2 * add(X(0), RX(0, "theta"))
    print(b)
    # After applying scale_primitive_blocks_only
    print(scale_primitive_blocks_only(b))
    ```
    """
    raise NotImplementedError(f"scale_primitive_blocks_only is not implemented for {type(block)}")

set_trainable(blocks, value=True, inplace=True)

Set the trainability of all parameters in a block to a given value

PARAMETER DESCRIPTION
blocks

Block or list of blocks for which to set the trainable attribute

TYPE: AbstractBlock | list[AbstractBlock]

value

The value of the trainable attribute to assign to the input blocks

TYPE: bool DEFAULT: True

inplace

Whether to modify the block(s) in place or not. Currently, only

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
NotImplementedError

if the inplace argument is set to False, the function will raise this exception

RETURNS DESCRIPTION
AbstractBlock | list[AbstractBlock]

AbstractBlock | list[AbstractBlock]: the input block or list of blocks with the trainable attribute set to the given value

Source code in qadence/transpile/block.py
def set_trainable(
    blocks: AbstractBlock | list[AbstractBlock], value: bool = True, inplace: bool = True
) -> AbstractBlock | list[AbstractBlock]:
    """Set the trainability of all parameters in a block to a given value

    Args:
        blocks (AbstractBlock | list[AbstractBlock]): Block or list of blocks for which
            to set the trainable attribute
        value (bool, optional): The value of the trainable attribute to assign to the input blocks
        inplace (bool, optional): Whether to modify the block(s) in place or not. Currently, only

    Raises:
        NotImplementedError: if the `inplace` argument is set to False, the function will
            raise  this exception

    Returns:
        AbstractBlock | list[AbstractBlock]: the input block or list of blocks with the trainable
            attribute set to the given value
    """

    if isinstance(blocks, AbstractBlock):
        blocks = [blocks]

    if inplace:
        for block in blocks:
            params: list[sympy.Basic] = parameters(block)
            for p in params:
                if not p.is_number:
                    p.trainable = value
    else:
        raise NotImplementedError("Not inplace set_trainable is not yet available")

    return blocks if len(blocks) > 1 else blocks[0]

validate(block)

Moves a block from global to local qubit numbers by adding PutBlocks and reassigning qubit locations approriately.

Example

from qadence.blocks import chain
from qadence.operations import X
from qadence.transpile import validate

x = chain(chain(X(0)), chain(X(1)))
print(x)
print(validate(x))
ChainBlock(0,1)
├── ChainBlock(0)
   └── X(0)
└── ChainBlock(1)
    └── X(1)
ChainBlock(0,1)
├── put on (0)
   └── ChainBlock(0)
       └── put on (0)
           └── X(0)
└── put on (1)
    └── ChainBlock(0)
        └── put on (0)
            └── X(0)
Source code in qadence/transpile/block.py
def validate(block: AbstractBlock) -> AbstractBlock:
    """Moves a block from global to local qubit numbers by adding PutBlocks and reassigning
    qubit locations approriately.

    # Example
    ```python exec="on" source="above" result="json"
    from qadence.blocks import chain
    from qadence.operations import X
    from qadence.transpile import validate

    x = chain(chain(X(0)), chain(X(1)))
    print(x)
    print(validate(x))
    ```
    """
    vblock: AbstractBlock
    from qadence.transpile import reassign

    if isinstance(block, ControlBlock):
        vblock = deepcopy(block)
        b: AbstractBlock
        (b,) = block.blocks
        b = reassign(b, {i: i - min(b.qubit_support) for i in b.qubit_support})
        b = validate(b)
        vblock.blocks = (b,)  # type: ignore[assignment]

    elif isinstance(block, CompositeBlock):
        blocks = []
        for b in block.blocks:
            mi, ma = min(b.qubit_support), max(b.qubit_support)
            nb = reassign(b, {i: i - min(b.qubit_support) for i in b.qubit_support})
            nb = validate(nb)
            nb = PutBlock(nb, tuple(range(mi, ma + 1)))
            blocks.append(nb)
        try:
            vblock = _construct(type(block), tuple(blocks))
        except AssertionError as e:
            if str(e) == "Make sure blocks act on distinct qubits!":
                vblock = chain(*blocks)
            else:
                raise e

    elif isinstance(block, PrimitiveBlock):
        vblock = deepcopy(block)

    else:
        raise NotImplementedError

    vblock.tag = block.tag
    return vblock

add_interaction(x, *args, interaction=Interaction.NN, spacing=1.0)

Turns blocks or circuits into (a chain of) HamEvo blocks including a chosen interaction term.

This is a @singledipatched function which can be called in three ways:

  • With a QuantumCircuit which contains all necessary information: add_interaction(circuit)
  • With a Register and an AbstractBlock: add_interaction(reg, block)
  • With an AbstractBlock only: add_interaction(block)

See the section about analog blocks for detailed information about how which types of blocks are translated.

PARAMETER DESCRIPTION
x

Circuit or block to be emulated. See the examples on which argument combinations are accepted.

TYPE: Register | QuantumCircuit | AbstractBlock

interaction

Type of interaction that is added. Can also be a function that accepts a register and a list of edges that define which qubits interact (see the examples).

TYPE: Interaction | Callable DEFAULT: NN

spacing

All qubit coordinates are multiplied by spacing.

TYPE: float DEFAULT: 1.0

Examples:

from qadence import QuantumCircuit, AnalogRX, add_interaction

c = QuantumCircuit(2, AnalogRX(2.0))
e = add_interaction(c)
[mul: 0.0] 
└── AddBlock(0,1)
    ├── AddBlock(0,1)
       └── AddBlock(0,1)
           ├── [mul: 1.571] 
              └── AddBlock(0,1)
                  ├── AddBlock(0)
                     ├── [mul: 1.00000000000000] 
                        └── X(0)
                     └── [mul: 0.0] 
                         └── Y(0)
                  └── AddBlock(1)
                      ├── [mul: 1.00000000000000] 
                         └── X(1)
                      └── [mul: 0.0] 
                          └── Y(1)
           └── [mul: 0.0] 
               └── AddBlock(0,1)
                   ├── N(0)
                   └── N(1)
    └── AddBlock(0,1)
        └── [mul: 865723.020] 
            └── KronBlock(0,1)
                ├── N(0)
                └── N(1)
You can also use add_interaction directly on a block, but you have to provide either the Register or define a non-global qubit support.
from qadence import AnalogRX, Register, add_interaction

b = AnalogRX(2.0)
r = Register(1)
e = add_interaction(r, b)

# or provide only the block with local qubit support
# in this case the register is created via `Register(b.n_qubits)`
e = add_interaction(AnalogRX(2.0, qubit_support=(0,)))
print(e.generator)
[mul: 0.450] 
└── AddBlock(0)
    └── AddBlock(0)
        ├── [mul: 1.571] 
           └── AddBlock(0)
               └── AddBlock(0)
                   ├── [mul: 1.00000000000000] 
                      └── X(0)
                   └── [mul: 0.0] 
                       └── Y(0)
        └── [mul: 0.0] 
            └── AddBlock(0)
                └── N(0)
[mul: 0.450] 
└── AddBlock(0)
    └── AddBlock(0)
        ├── [mul: 1.571] 
           └── AddBlock(0)
               └── AddBlock(0)
                   ├── [mul: 1.00000000000000] 
                      └── X(0)
                   └── [mul: 0.0] 
                       └── Y(0)
        └── [mul: 0.0] 
            └── AddBlock(0)
                └── N(0)
You can specify a custom interaction function which has to accept a Register and a list of edges: list[tuple[int, int]]:
from qadence import AnalogRX, Register, add_interaction
from qadence.transpile.emulate import ising_interaction

def int_fn(r: Register, pairs: list[tuple[int, int]]) -> AbstractBlock:
    # do either something completely custom
    # ...
    # or e.g. change the default kwargs to `ising_interaction`
    return ising_interaction(r, pairs, rydberg_level=70)

b = AnalogRX(2.0)
r = Register(1)
e = add_interaction(r, b, interaction=int_fn)

Source code in qadence/transpile/emulate.py
@singledispatch
def add_interaction(
    x: Register | QuantumCircuit | AbstractBlock,
    *args: Any,
    interaction: Interaction | Callable = Interaction.NN,
    spacing: float = 1.0,
) -> QuantumCircuit | AbstractBlock:
    """Turns blocks or circuits into (a chain of) `HamEvo` blocks including a
    chosen interaction term.

    This is a `@singledipatch`ed function which can be called in three ways:

    * With a `QuantumCircuit` which contains all necessary information: `add_interaction(circuit)`
    * With a `Register` and an `AbstractBlock`: `add_interaction(reg, block)`
    * With an `AbstractBlock` only: `add_interaction(block)`

    See the section about [analog blocks](/digital_analog_qc/analog-basics.md) for
    detailed information about how which types of blocks are translated.

    Arguments:
        x: Circuit or block to be emulated. See the examples on which argument
            combinations are accepted.
        interaction: Type of interaction that is added. Can also be a function that accepts a
            register and a list of edges that define which qubits interact (see the examples).
        spacing: All qubit coordinates are multiplied by `spacing`.

    Examples:
    ```python exec="on" source="material-block" result="json"
    from qadence import QuantumCircuit, AnalogRX, add_interaction

    c = QuantumCircuit(2, AnalogRX(2.0))
    e = add_interaction(c)
    print(str(e.block.generator)) # markdown-exec: hide
    ```
    You can also use `add_interaction` directly on a block, but you have to provide either
    the `Register` or define a non-global qubit support.
    ```python exec="on" source="material-block" result="json"
    from qadence import AnalogRX, Register, add_interaction

    b = AnalogRX(2.0)
    r = Register(1)
    e = add_interaction(r, b)
    print(e.generator) # markdown-exec: hide

    # or provide only the block with local qubit support
    # in this case the register is created via `Register(b.n_qubits)`
    e = add_interaction(AnalogRX(2.0, qubit_support=(0,)))
    print(e.generator)
    ```
    You can specify a custom `interaction` function which has to accept a `Register` and a list
    of `edges: list[tuple[int, int]]`:
    ```python exec="on" source="material-block" result="json"
    from qadence import AnalogRX, Register, add_interaction
    from qadence.transpile.emulate import ising_interaction

    def int_fn(r: Register, pairs: list[tuple[int, int]]) -> AbstractBlock:
        # do either something completely custom
        # ...
        # or e.g. change the default kwargs to `ising_interaction`
        return ising_interaction(r, pairs, rydberg_level=70)

    b = AnalogRX(2.0)
    r = Register(1)
    e = add_interaction(r, b, interaction=int_fn)
    ```
    """
    raise ValueError(f"`add_interaction` is not implemented for {type(x)}")