Skip to content

Block system

Quantum programs in Qadence are constructed using a block-system, with an emphasis on composability of primitive blocks to obtain larger, composite blocks. This functional approach is different from other frameworks which follow a more object-oriented way to construct circuits and express programs.

Primitive blocks

A PrimitiveBlock represents a digital or an analog time-evolution quantum operation applied to a qubit support. Programs can always be decomposed down into a sequence of PrimitiveBlock elements.

Two canonical examples of digital primitive blocks are the parametrized RX and the CNOT gates:

from qadence import chain, RX, CNOT

rx = RX(0, 0.5)
cnot = CNOT(0, 1)

block = chain(rx, cnot)
%3 912751ed362c4272b0a13a4e51bef45d 0 448596d7246546449078a739826fdc2d RX(0.5) 912751ed362c4272b0a13a4e51bef45d--448596d7246546449078a739826fdc2d 046c9cdd7a8d4bd3ab76753b4153efb5 1 c0440b1ad47a4e78ab54bba40519dde9 448596d7246546449078a739826fdc2d--c0440b1ad47a4e78ab54bba40519dde9 093673f8bdc747028a050faf01b8f9f0 c0440b1ad47a4e78ab54bba40519dde9--093673f8bdc747028a050faf01b8f9f0 594edd46a26645f9be22c5d3fdd03157 ecf35acef84f4414ab1429e9c32733f3 046c9cdd7a8d4bd3ab76753b4153efb5--ecf35acef84f4414ab1429e9c32733f3 a542d7c7cc1147b48ae2fc17b09d6c32 X ecf35acef84f4414ab1429e9c32733f3--a542d7c7cc1147b48ae2fc17b09d6c32 a542d7c7cc1147b48ae2fc17b09d6c32--c0440b1ad47a4e78ab54bba40519dde9 a542d7c7cc1147b48ae2fc17b09d6c32--594edd46a26645f9be22c5d3fdd03157

A list of all available primitive operations can be found here.

How to visualize blocks

There are two ways to display blocks in a Python interpreter: either as a tree in ASCII format using print:

from qadence import X, Y, kron

kron_block = kron(X(0), Y(1))
print(kron_block)
KronBlock(0,1)
├── X(0)
└── Y(1)

Or using the visualization package:

from qadence import X, Y, kron
from qadence.draw import display

kron_block = kron(X(0), Y(1))
# display(kron_block)
%3 d5a1cb4a8a2f4202be600334a9327552 0 069ac1381c0842b9bbeec85efadae184 X d5a1cb4a8a2f4202be600334a9327552--069ac1381c0842b9bbeec85efadae184 2e48ee099167485aa578193fbb4fddaf 1 368370a9100d453fa545b58aabe57f98 069ac1381c0842b9bbeec85efadae184--368370a9100d453fa545b58aabe57f98 c3861004942841dfa1bd39aa65dcef2c 95aea282043a45ee95684e51d8a7eb67 Y 2e48ee099167485aa578193fbb4fddaf--95aea282043a45ee95684e51d8a7eb67 95aea282043a45ee95684e51d8a7eb67--c3861004942841dfa1bd39aa65dcef2c

Composite Blocks

Programs can be expressed by composing blocks to result in a larger CompositeBlock using three fundamental operations: chain, kron, and add.

  • chain applies a set of blocks in sequence, which can have overlapping qubit supports, and results in a ChainBlock type. It is akin to applying a matrix product of the sub-blocks, and can also be used with the * operator.
  • kron applies a set of blocks in parallel, requiring disjoint qubit support, and results in a KronBlock type. This is akin to applying a tensor product of the sub-blocks, and can also be used with the @ operator.
  • add performs a direct sum of the operators, and results in an AddBlock type. Blocks constructed this way are typically non-unitary, as is the case for Hamiltonians which can be constructed through sums of Pauli strings. Addition can also be performed directly with the + operator.
from qadence import X, Y, chain, kron

chain_0 = chain(X(0), Y(0))
chain_1 = chain(X(1), Y(1))

kron_block = kron(chain_0, chain_1)
%3 4eabd43c3e464418bcd871489382fe12 0 5a8c8041c3f14ed8b7b4440d4a696fe0 X 4eabd43c3e464418bcd871489382fe12--5a8c8041c3f14ed8b7b4440d4a696fe0 3a8b10d6c4314642a8cbd1462368b4e8 1 a93aeb0c55c144ea82095f5559470d8f Y 5a8c8041c3f14ed8b7b4440d4a696fe0--a93aeb0c55c144ea82095f5559470d8f 6ff96597cb3c43bcbb716ba7bfd5bf6c a93aeb0c55c144ea82095f5559470d8f--6ff96597cb3c43bcbb716ba7bfd5bf6c 0f659067651043948a104a23ebf03dcc 44a6aa82e56b4880b96947d6747bfc13 X 3a8b10d6c4314642a8cbd1462368b4e8--44a6aa82e56b4880b96947d6747bfc13 ac785394c3e244b9979f8dff87efb3fb Y 44a6aa82e56b4880b96947d6747bfc13--ac785394c3e244b9979f8dff87efb3fb ac785394c3e244b9979f8dff87efb3fb--0f659067651043948a104a23ebf03dcc

All composition functions support list comprehension syntax. Below we exemplify the creation of an XY Hamiltonian for qubits laid out on a line.

from qadence import X, Y, add

def xy_int(i: int, j: int):
    return (1/2) * (X(i)@X(j) + Y(i)@Y(j))

n_qubits = 3

xy_ham = add(xy_int(i, i+1) for i in range(n_qubits-1))
AddBlock(0,1,2)
├── [mul: 0.500] 
   └── AddBlock(0,1)
       ├── KronBlock(0,1)
          ├── X(0)
          └── X(1)
       └── KronBlock(0,1)
           ├── Y(0)
           └── Y(1)
└── [mul: 0.500] 
    └── AddBlock(1,2)
        ├── KronBlock(1,2)
           ├── X(1)
           └── X(2)
        └── KronBlock(1,2)
            ├── Y(1)
            └── Y(2)

Qadence blocks can be directly translated to matrix form by calling block.tensor(). Note that first dimension is the batch dimension, following PyTorch conventions. This becomes relevant if the block are parameterized and batched input values are passed, as we will see later.

from qadence import X, Y

xy = (1/2) * (X(0)@X(1) + Y(0)@Y(1))

print(xy.tensor().real)
tensor([[[0., 0., 0., 0.],
         [0., 0., 1., 0.],
         [0., 1., 0., 0.],
         [0., 0., 0., 0.]]])

For a final example of the flexibility of functional block composition, below is an implementation of the Quantum Fourier Transform on an arbitrary qubit support.

from qadence import H, CPHASE, PI, chain, kron

def qft_layer(qs: tuple, l: int):
    cphases = chain(CPHASE(qs[j], qs[l], PI/2**(j-l)) for j in range(l+1, len(qs)))
    return H(qs[l]) * cphases

def qft(qs: tuple):
    return chain(qft_layer(qs, l) for l in range(len(qs)))
%3 06bcb0ec51f64923b95d6d1a72a5d884 0 0888d71d6cb942aca576c539138c5164 H 06bcb0ec51f64923b95d6d1a72a5d884--0888d71d6cb942aca576c539138c5164 fe9f1f3e954548fba4253ae5836d1a09 1 f35b75dd98bf4fedb61946747cd91ed5 PHASE(1.571) 0888d71d6cb942aca576c539138c5164--f35b75dd98bf4fedb61946747cd91ed5 54f5ada9bfb24f7390753c9e027a2c09 PHASE(0.785) f35b75dd98bf4fedb61946747cd91ed5--54f5ada9bfb24f7390753c9e027a2c09 129ebf291ce643988299128cedcb1a47 f35b75dd98bf4fedb61946747cd91ed5--129ebf291ce643988299128cedcb1a47 e53afd8d1ca2497c854c08e79f9fec78 54f5ada9bfb24f7390753c9e027a2c09--e53afd8d1ca2497c854c08e79f9fec78 a13ee1a6beef468d853b35e3866568c2 54f5ada9bfb24f7390753c9e027a2c09--a13ee1a6beef468d853b35e3866568c2 d3e6f5955b86458480b05e91d8c0af28 e53afd8d1ca2497c854c08e79f9fec78--d3e6f5955b86458480b05e91d8c0af28 068d96e1c3f54b8c86d964f9c130f848 d3e6f5955b86458480b05e91d8c0af28--068d96e1c3f54b8c86d964f9c130f848 69f347935d634da6b5d7bced0921d7f2 068d96e1c3f54b8c86d964f9c130f848--69f347935d634da6b5d7bced0921d7f2 dd71c715c76f497588df9abfc569d3e7 6c9b490857d044b09ca701d5cc9e0a24 fe9f1f3e954548fba4253ae5836d1a09--6c9b490857d044b09ca701d5cc9e0a24 0c259dbc804d4e2e995d76597b0b040b 2 6c9b490857d044b09ca701d5cc9e0a24--129ebf291ce643988299128cedcb1a47 3d6a3d09f7b142eda2986f91c6c652d4 129ebf291ce643988299128cedcb1a47--3d6a3d09f7b142eda2986f91c6c652d4 d6d2ffe694b34273aa3b8eba71a2815b H 3d6a3d09f7b142eda2986f91c6c652d4--d6d2ffe694b34273aa3b8eba71a2815b 87dd5d3ffbb04c7ca94eebc040242f5f PHASE(1.571) d6d2ffe694b34273aa3b8eba71a2815b--87dd5d3ffbb04c7ca94eebc040242f5f 19a1208719554abeb169e1bf860ace2a 87dd5d3ffbb04c7ca94eebc040242f5f--19a1208719554abeb169e1bf860ace2a 0e7bdcdd76a749aba8a48550554c851d 87dd5d3ffbb04c7ca94eebc040242f5f--0e7bdcdd76a749aba8a48550554c851d 19a1208719554abeb169e1bf860ace2a--dd71c715c76f497588df9abfc569d3e7 f5588ba449584d13bd361259397b7974 0dceb24d86724834bdb3bdd79c10b7b7 0c259dbc804d4e2e995d76597b0b040b--0dceb24d86724834bdb3bdd79c10b7b7 a01eb7124f64488b95c76fa5137bf4ff 0dceb24d86724834bdb3bdd79c10b7b7--a01eb7124f64488b95c76fa5137bf4ff a01eb7124f64488b95c76fa5137bf4ff--a13ee1a6beef468d853b35e3866568c2 85fbec1149b74e74be5880b744e2f4a5 a13ee1a6beef468d853b35e3866568c2--85fbec1149b74e74be5880b744e2f4a5 85fbec1149b74e74be5880b744e2f4a5--0e7bdcdd76a749aba8a48550554c851d 229a0bbba3f84a7f9ace08bcb77bd607 H 0e7bdcdd76a749aba8a48550554c851d--229a0bbba3f84a7f9ace08bcb77bd607 229a0bbba3f84a7f9ace08bcb77bd607--f5588ba449584d13bd361259397b7974

Other functionalities are directly built in the block system. For example, the inverse operation can be created with the dagger() method.

qft_inv = qft((0, 1, 2)).dagger()
%3 3b887fc0c9704c71b38e948eb9385b63 0 20eeb1b1e125483885ddff36f7979ed7 3b887fc0c9704c71b38e948eb9385b63--20eeb1b1e125483885ddff36f7979ed7 70231bd3ad174cde97e30809454eca5a 1 d5c3a3258a11409e91bc5914502ee282 20eeb1b1e125483885ddff36f7979ed7--d5c3a3258a11409e91bc5914502ee282 c5c9d08b2c364acdbbfc60ff3b29467c d5c3a3258a11409e91bc5914502ee282--c5c9d08b2c364acdbbfc60ff3b29467c dd634cf7b1ad45f6936fdaeb6c6ffe8a PHASE(-0.785) c5c9d08b2c364acdbbfc60ff3b29467c--dd634cf7b1ad45f6936fdaeb6c6ffe8a 6cae0f14d44a4cbbbb9d878017d592ae PHASE(-1.571) dd634cf7b1ad45f6936fdaeb6c6ffe8a--6cae0f14d44a4cbbbb9d878017d592ae 0ee8f01f030445f8822a4070f71af2d9 dd634cf7b1ad45f6936fdaeb6c6ffe8a--0ee8f01f030445f8822a4070f71af2d9 a431ffa49a23424d9cd02d6e8448c8a0 H 6cae0f14d44a4cbbbb9d878017d592ae--a431ffa49a23424d9cd02d6e8448c8a0 7c341fdf413b4761acaf637db0ad1c8a 6cae0f14d44a4cbbbb9d878017d592ae--7c341fdf413b4761acaf637db0ad1c8a cbfeda05ce8e4b968e484f869ff0748c a431ffa49a23424d9cd02d6e8448c8a0--cbfeda05ce8e4b968e484f869ff0748c 95a3406b353149a3a4e07b6f76eea399 42bbf990e1824cc791295add8192018d 70231bd3ad174cde97e30809454eca5a--42bbf990e1824cc791295add8192018d cd2684587cfe4fd59558ed4fe40f6d32 2 51a09b4284154984a7e8aad700070c34 PHASE(-1.571) 42bbf990e1824cc791295add8192018d--51a09b4284154984a7e8aad700070c34 4e347f7d43b2409aab663657f8de1959 H 51a09b4284154984a7e8aad700070c34--4e347f7d43b2409aab663657f8de1959 44177f0e66a04c53be6aba7c0f5e4448 51a09b4284154984a7e8aad700070c34--44177f0e66a04c53be6aba7c0f5e4448 76b0680f6b32455e9bbc63a553e27b7d 4e347f7d43b2409aab663657f8de1959--76b0680f6b32455e9bbc63a553e27b7d 76b0680f6b32455e9bbc63a553e27b7d--7c341fdf413b4761acaf637db0ad1c8a bc6df54394514b17be5869d8d8d6342f 7c341fdf413b4761acaf637db0ad1c8a--bc6df54394514b17be5869d8d8d6342f bc6df54394514b17be5869d8d8d6342f--95a3406b353149a3a4e07b6f76eea399 24a2ce6bc723466f8546c30ce25bbf1c cdca996c7cf64147bcdfad4daa8ef057 H cd2684587cfe4fd59558ed4fe40f6d32--cdca996c7cf64147bcdfad4daa8ef057 cdca996c7cf64147bcdfad4daa8ef057--44177f0e66a04c53be6aba7c0f5e4448 6973a28b50df4eeb923469e088399e51 44177f0e66a04c53be6aba7c0f5e4448--6973a28b50df4eeb923469e088399e51 6973a28b50df4eeb923469e088399e51--0ee8f01f030445f8822a4070f71af2d9 8534f493727b4034a048cb31b3e43d71 0ee8f01f030445f8822a4070f71af2d9--8534f493727b4034a048cb31b3e43d71 afd0167764de4421b7a50397d4cddc39 8534f493727b4034a048cb31b3e43d71--afd0167764de4421b7a50397d4cddc39 afd0167764de4421b7a50397d4cddc39--24a2ce6bc723466f8546c30ce25bbf1c

Digital-analog composition

In Qadence, analog operations are first-class citizens. An analog operation is one whose unitary is best described by the evolution of some hermitian generator, or Hamiltonian, acting on an arbitrary number of qubits. Qadence provides the HamEvo class to initialize analog operations. For a time-independent generator \(\mathcal{H}\) and some time variable \(t\), HamEvo(H, t) represents the evolution operator \(\exp(-i\mathcal{H}t)\).

Analog operations constitute a generalization of digital operations, and all digital operations can also be represented as the evolution of some hermitian generator. For example, the RX gate is the evolution of X.

from qadence import X, RX, HamEvo, PI
from torch import allclose

angle = PI/2

block_digital = RX(0, angle)

block_analog = HamEvo(0.5*X(0), angle)

print(allclose(block_digital.tensor(), block_analog.tensor()))
True

As seen in the previous section, arbitrary Hamiltonians can be constructed using Pauli operators. Their evolution can be combined with other arbitrary digital operations and incorporated into any quantum program.

from qadence import X, Y, RX, HamEvo
from qadence import add, kron, PI

def xy_int(i: int, j: int):
    return (1/2) * (X(i)@X(j) + Y(i)@Y(j))

n_qubits = 3

xy_ham = add(xy_int(i, i+1) for i in range(n_qubits-1))

analog_evo = HamEvo(xy_ham, 1.0)

digital_block = kron(RX(i, i*PI/2) for i in range(n_qubits))

program = digital_block * analog_evo * digital_block
%3 cluster_58a8ccbe676743afb3a9d6cc2838053f 49d213041a274b45beacefc4958c7361 0 50b35187b6034d529a56830f122f6d0f RX(0.0) 49d213041a274b45beacefc4958c7361--50b35187b6034d529a56830f122f6d0f f4bbba66fcd649d8bb4d290de5a78cb5 1 2e013268ecf14ac09b0d588924462393 HamEvo 50b35187b6034d529a56830f122f6d0f--2e013268ecf14ac09b0d588924462393 adf471619dd4430c9d68a1369011813d RX(0.0) 2e013268ecf14ac09b0d588924462393--adf471619dd4430c9d68a1369011813d 1351250ff9a141c9881de4b1d89b1b40 adf471619dd4430c9d68a1369011813d--1351250ff9a141c9881de4b1d89b1b40 9f9ae8ff122643a9a9ab5b8a9383c7c8 e955f9c898a446c08eec7f9ceb7082b7 RX(1.571) f4bbba66fcd649d8bb4d290de5a78cb5--e955f9c898a446c08eec7f9ceb7082b7 f1d1902874c24f1fae39e270ed692431 2 b562b5494f9946c8bb52db6ce85da834 t = 1.000 e955f9c898a446c08eec7f9ceb7082b7--b562b5494f9946c8bb52db6ce85da834 fc1a986a1f98468c9633e43586450cfc RX(1.571) b562b5494f9946c8bb52db6ce85da834--fc1a986a1f98468c9633e43586450cfc fc1a986a1f98468c9633e43586450cfc--9f9ae8ff122643a9a9ab5b8a9383c7c8 8dfb3e9678ac462d8c6015d03aeeb334 32065c6c5bb14117904ef0ac20bc2c00 RX(3.142) f1d1902874c24f1fae39e270ed692431--32065c6c5bb14117904ef0ac20bc2c00 12fbc49fac534ecda6312cbb274d1264 32065c6c5bb14117904ef0ac20bc2c00--12fbc49fac534ecda6312cbb274d1264 c0eb84c6e7884cdcb7875c47973b727c RX(3.142) 12fbc49fac534ecda6312cbb274d1264--c0eb84c6e7884cdcb7875c47973b727c c0eb84c6e7884cdcb7875c47973b727c--8dfb3e9678ac462d8c6015d03aeeb334

Block execution

To quickly run block operations and access wavefunctions, samples or expectation values of observables, one can use the convenience functions run, sample and expectation.

from qadence import kron, add, H, Z, run, sample, expectation

n_qubits = 2

# Prepares a uniform state
h_block = kron(H(i) for i in range(n_qubits))

wf = run(h_block)

xs = sample(h_block, n_shots=1000)

obs = add(Z(i) for i in range(n_qubits))
ex = expectation(h_block, obs)
wf = tensor([[0.5000+0.j, 0.5000+0.j, 0.5000+0.j, 0.5000+0.j]])
xs = [OrderedCounter({'01': 264, '11': 257, '00': 249, '10': 230})]
ex = tensor([[0.]])

Execution via QuantumCircuit and QuantumModel

More fine-grained control and better performance is provided via the high-level QuantumModel abstraction. Quantum programs in Qadence are constructed in two steps:

  1. Build a QuantumCircuit which ties together a composite block and a register.
  2. Define a QuantumModel which differentiates, compiles and executes the circuit.

Execution of more complex Qadence programs will be explored in the next tutorials.

Adding noise to gates

It is possible to add noise to gates. Please refer to the noise tutorial here.