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 bda4314bbf0d495e8f7ac89726511918 0 a4447eb1d0674ce08ebf575037d08057 RX(0.5) bda4314bbf0d495e8f7ac89726511918--a4447eb1d0674ce08ebf575037d08057 087e873577c74a4486b59189366831b6 1 d6bdead7a91f49cb93507b47cd24f5db a4447eb1d0674ce08ebf575037d08057--d6bdead7a91f49cb93507b47cd24f5db 015154d163804778a24af88fd979592f d6bdead7a91f49cb93507b47cd24f5db--015154d163804778a24af88fd979592f e7e2f6d34eb54c8b9949a8c1c6e97093 484e4a8885584412bf66ee094c6313e9 087e873577c74a4486b59189366831b6--484e4a8885584412bf66ee094c6313e9 2fe1fb3f16fa4431bcf3201ca3c8b06c X 484e4a8885584412bf66ee094c6313e9--2fe1fb3f16fa4431bcf3201ca3c8b06c 2fe1fb3f16fa4431bcf3201ca3c8b06c--d6bdead7a91f49cb93507b47cd24f5db 2fe1fb3f16fa4431bcf3201ca3c8b06c--e7e2f6d34eb54c8b9949a8c1c6e97093

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 da300b4b33554deabd4ec7f91d95073b 0 a0e4d5d2edf24fadaca658f19d7829dd X da300b4b33554deabd4ec7f91d95073b--a0e4d5d2edf24fadaca658f19d7829dd 59023c1a88304c0f84572da521fd1f00 1 e05ab8d25dcb47358df98d7ffdfa0cde a0e4d5d2edf24fadaca658f19d7829dd--e05ab8d25dcb47358df98d7ffdfa0cde 7cafc6c40621485eb32f6cda73a2a633 6e633a67a3304b7fb1dc258389282a89 Y 59023c1a88304c0f84572da521fd1f00--6e633a67a3304b7fb1dc258389282a89 6e633a67a3304b7fb1dc258389282a89--7cafc6c40621485eb32f6cda73a2a633

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 8bdd9801066a443cad8b9d56071e3e8b 0 d3e4c114f4aa4621b82c833be420ae8c X 8bdd9801066a443cad8b9d56071e3e8b--d3e4c114f4aa4621b82c833be420ae8c 87263b7071a74e95a5f0927327e5abc2 1 505f358032484905b6a35038ed49b86e Y d3e4c114f4aa4621b82c833be420ae8c--505f358032484905b6a35038ed49b86e 4b2ad8672c0447a088f074c62d4e5b8b 505f358032484905b6a35038ed49b86e--4b2ad8672c0447a088f074c62d4e5b8b 31c6f59a94f34795aced8068caf5a8d8 85d4180b5aad49f3b0b3fe74d883e6cc X 87263b7071a74e95a5f0927327e5abc2--85d4180b5aad49f3b0b3fe74d883e6cc 61419e717c8e4e16bf7d22cc0cee2e01 Y 85d4180b5aad49f3b0b3fe74d883e6cc--61419e717c8e4e16bf7d22cc0cee2e01 61419e717c8e4e16bf7d22cc0cee2e01--31c6f59a94f34795aced8068caf5a8d8

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 dfdd90012ff04bd3a0ce08c41119d308 0 e42b4e95e73940bba534ab8b464af969 H dfdd90012ff04bd3a0ce08c41119d308--e42b4e95e73940bba534ab8b464af969 56880d82f5774eeca3b6b733f863a17f 1 504213d039f34e1eabc78e7bc76c7663 PHASE(1.571) e42b4e95e73940bba534ab8b464af969--504213d039f34e1eabc78e7bc76c7663 fc17db436ce04a9497fcacbeb8b623b4 PHASE(0.785) 504213d039f34e1eabc78e7bc76c7663--fc17db436ce04a9497fcacbeb8b623b4 06f8a3a96fa04a58b475fdf1e461a839 504213d039f34e1eabc78e7bc76c7663--06f8a3a96fa04a58b475fdf1e461a839 469fbea402b245e594e1f4c4c43975dd fc17db436ce04a9497fcacbeb8b623b4--469fbea402b245e594e1f4c4c43975dd 1834082899d1467aa1a66d69bdab796a fc17db436ce04a9497fcacbeb8b623b4--1834082899d1467aa1a66d69bdab796a 8501509cd3614400a957521ab060926e 469fbea402b245e594e1f4c4c43975dd--8501509cd3614400a957521ab060926e 08293d6c86ab4fe58e8fbbc56ec0160d 8501509cd3614400a957521ab060926e--08293d6c86ab4fe58e8fbbc56ec0160d 5a4105a2d97741aba7af23bdac4f1059 08293d6c86ab4fe58e8fbbc56ec0160d--5a4105a2d97741aba7af23bdac4f1059 6d5c277450984c7b9d9270e1ed161399 b09ad7b5de624872b75ceaf992b41540 56880d82f5774eeca3b6b733f863a17f--b09ad7b5de624872b75ceaf992b41540 74bdbdefe53043c0a5fb4c352742a64e 2 b09ad7b5de624872b75ceaf992b41540--06f8a3a96fa04a58b475fdf1e461a839 62fe1eb607054589bf22835b3c0492be 06f8a3a96fa04a58b475fdf1e461a839--62fe1eb607054589bf22835b3c0492be bd9f235bddba426b94a18000159a6994 H 62fe1eb607054589bf22835b3c0492be--bd9f235bddba426b94a18000159a6994 e721ff3f05734726a5c2810636e8f7e3 PHASE(1.571) bd9f235bddba426b94a18000159a6994--e721ff3f05734726a5c2810636e8f7e3 4ccc7b80e7da49a3b89833f023636ac0 e721ff3f05734726a5c2810636e8f7e3--4ccc7b80e7da49a3b89833f023636ac0 b6b99382d8bc46d9a1bf94863dfc1262 e721ff3f05734726a5c2810636e8f7e3--b6b99382d8bc46d9a1bf94863dfc1262 4ccc7b80e7da49a3b89833f023636ac0--6d5c277450984c7b9d9270e1ed161399 13c8769820b441c6bc9ef7c52610ff55 9533fff760f647fcadf5a4d5706b053a 74bdbdefe53043c0a5fb4c352742a64e--9533fff760f647fcadf5a4d5706b053a d998e769c2b845e8aec6e9737480f7c5 9533fff760f647fcadf5a4d5706b053a--d998e769c2b845e8aec6e9737480f7c5 d998e769c2b845e8aec6e9737480f7c5--1834082899d1467aa1a66d69bdab796a 7b68075d04584a5b8c1314b5ff47b7f6 1834082899d1467aa1a66d69bdab796a--7b68075d04584a5b8c1314b5ff47b7f6 7b68075d04584a5b8c1314b5ff47b7f6--b6b99382d8bc46d9a1bf94863dfc1262 6da89df70a1248af93c76fb9d0739410 H b6b99382d8bc46d9a1bf94863dfc1262--6da89df70a1248af93c76fb9d0739410 6da89df70a1248af93c76fb9d0739410--13c8769820b441c6bc9ef7c52610ff55

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 b56cb52470894f3b800d841ff3a7fb02 0 8fe84affb71b4c54a24d68bce2de7dcd b56cb52470894f3b800d841ff3a7fb02--8fe84affb71b4c54a24d68bce2de7dcd e3107858227e4632be3482b70cafe312 1 db60f19b2aab4ad58a59a5793069f0fe 8fe84affb71b4c54a24d68bce2de7dcd--db60f19b2aab4ad58a59a5793069f0fe c64f9518ee79496f83e14181db56ab44 db60f19b2aab4ad58a59a5793069f0fe--c64f9518ee79496f83e14181db56ab44 0983d3b20fab4a86903ed8c9c41eb83b PHASE(-0.785) c64f9518ee79496f83e14181db56ab44--0983d3b20fab4a86903ed8c9c41eb83b f72b1b38e4934028acec8441dea2e1ec PHASE(-1.571) 0983d3b20fab4a86903ed8c9c41eb83b--f72b1b38e4934028acec8441dea2e1ec 4a3e0b4ea9044115b74b573c50d444ae 0983d3b20fab4a86903ed8c9c41eb83b--4a3e0b4ea9044115b74b573c50d444ae 04b5afaae22d41e29fee86423053fb57 H f72b1b38e4934028acec8441dea2e1ec--04b5afaae22d41e29fee86423053fb57 7ab438605b464949a94ba0430a770e9a f72b1b38e4934028acec8441dea2e1ec--7ab438605b464949a94ba0430a770e9a 5e8b5aa8d4664241be1da9735ed5c2ef 04b5afaae22d41e29fee86423053fb57--5e8b5aa8d4664241be1da9735ed5c2ef a80c0841911f42ba86768f5eecb8419c 4cb1a2d6365b4db181d1c6932342e93c e3107858227e4632be3482b70cafe312--4cb1a2d6365b4db181d1c6932342e93c 5ddc70b809764b7388a2db70285ae476 2 1e6aaa3007684f839833ace78352cfb6 PHASE(-1.571) 4cb1a2d6365b4db181d1c6932342e93c--1e6aaa3007684f839833ace78352cfb6 624f5b206427445a964feaf6256905f6 H 1e6aaa3007684f839833ace78352cfb6--624f5b206427445a964feaf6256905f6 a0bae8936bf84989915a33e8804a0cab 1e6aaa3007684f839833ace78352cfb6--a0bae8936bf84989915a33e8804a0cab 99d04e8f60754c858d47f7b9cc74e727 624f5b206427445a964feaf6256905f6--99d04e8f60754c858d47f7b9cc74e727 99d04e8f60754c858d47f7b9cc74e727--7ab438605b464949a94ba0430a770e9a fd07e67059c940c99e40da8c6ed435d3 7ab438605b464949a94ba0430a770e9a--fd07e67059c940c99e40da8c6ed435d3 fd07e67059c940c99e40da8c6ed435d3--a80c0841911f42ba86768f5eecb8419c f706e640a93b43b9b20cdd603488b8eb 469e569f80cf443cb6fb99701adc21ae H 5ddc70b809764b7388a2db70285ae476--469e569f80cf443cb6fb99701adc21ae 469e569f80cf443cb6fb99701adc21ae--a0bae8936bf84989915a33e8804a0cab b411f1ff5dc84b89b1239130c9e4bfc3 a0bae8936bf84989915a33e8804a0cab--b411f1ff5dc84b89b1239130c9e4bfc3 b411f1ff5dc84b89b1239130c9e4bfc3--4a3e0b4ea9044115b74b573c50d444ae 9ab6bd37921e4db18de4cd33a92d5948 4a3e0b4ea9044115b74b573c50d444ae--9ab6bd37921e4db18de4cd33a92d5948 b96e09e9ab7844b1b2f41a35cf96ca1a 9ab6bd37921e4db18de4cd33a92d5948--b96e09e9ab7844b1b2f41a35cf96ca1a b96e09e9ab7844b1b2f41a35cf96ca1a--f706e640a93b43b9b20cdd603488b8eb

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_e4854481bb404e06abac7241c93b761d 5a62b8f8e0d24a1081cecce3267e3a51 0 3066245d58a148edacf1539703d6bb1f RX(0.0) 5a62b8f8e0d24a1081cecce3267e3a51--3066245d58a148edacf1539703d6bb1f a8ccf3c256c14d5da59e82360bc35250 1 de60634cbe6645afa7ecdfd73d55e56a HamEvo 3066245d58a148edacf1539703d6bb1f--de60634cbe6645afa7ecdfd73d55e56a 272399fea8ca4530b12bf63c6f26f8dd RX(0.0) de60634cbe6645afa7ecdfd73d55e56a--272399fea8ca4530b12bf63c6f26f8dd 7cf7d7e7c1ee41d585066f3516e1d8e3 272399fea8ca4530b12bf63c6f26f8dd--7cf7d7e7c1ee41d585066f3516e1d8e3 bbadb17c49514c34ae559e4c557959bd 00cdd6319c614f44a53ef97dce59a231 RX(1.571) a8ccf3c256c14d5da59e82360bc35250--00cdd6319c614f44a53ef97dce59a231 d2335a8b7b404c1a83feb65e3876c6c3 2 df9ce5c8d64e47f18171cbae9b44ec37 t = 1.000 00cdd6319c614f44a53ef97dce59a231--df9ce5c8d64e47f18171cbae9b44ec37 187c0d402800462ba401f864f1709ebe RX(1.571) df9ce5c8d64e47f18171cbae9b44ec37--187c0d402800462ba401f864f1709ebe 187c0d402800462ba401f864f1709ebe--bbadb17c49514c34ae559e4c557959bd 096bba498304464d811b8a6eec4b1ada 78055d1eef1d46f9a2847a92604d7e5f RX(3.142) d2335a8b7b404c1a83feb65e3876c6c3--78055d1eef1d46f9a2847a92604d7e5f ca72314e10d14a918d7c9a243e934c58 78055d1eef1d46f9a2847a92604d7e5f--ca72314e10d14a918d7c9a243e934c58 af22527e8034441b901ec56d025cd6a8 RX(3.142) ca72314e10d14a918d7c9a243e934c58--af22527e8034441b901ec56d025cd6a8 af22527e8034441b901ec56d025cd6a8--096bba498304464d811b8a6eec4b1ada

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 = [Counter({'10': 269, '11': 247, '01': 246, '00': 238})]
ex = tensor([[0.]], requires_grad=True)

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.