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 5b8a69db4f3146a88727681e61497a0a 0 849aacfe160c441ca98707ab4a1af211 RX(0.5) 5b8a69db4f3146a88727681e61497a0a--849aacfe160c441ca98707ab4a1af211 d559c784b92b40c48dfcea8c62c0b755 1 bcd3d509ddb84d89bcaf802d39639032 849aacfe160c441ca98707ab4a1af211--bcd3d509ddb84d89bcaf802d39639032 bdbab5c002e6408e8b041c7bf3f4973e bcd3d509ddb84d89bcaf802d39639032--bdbab5c002e6408e8b041c7bf3f4973e c6fb0501a7c346259924bec0b8e86407 f8f82db9b2a947b78afe0d8b98f69de6 d559c784b92b40c48dfcea8c62c0b755--f8f82db9b2a947b78afe0d8b98f69de6 c8b35c048d29411880e88c5495316efb X f8f82db9b2a947b78afe0d8b98f69de6--c8b35c048d29411880e88c5495316efb c8b35c048d29411880e88c5495316efb--bcd3d509ddb84d89bcaf802d39639032 c8b35c048d29411880e88c5495316efb--c6fb0501a7c346259924bec0b8e86407

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 7e4224bc176342c191becdd2b63b3af7 0 24315944e1fb439fa1bc0c06fbeaa548 X 7e4224bc176342c191becdd2b63b3af7--24315944e1fb439fa1bc0c06fbeaa548 036b0eb57fea4304b084614164ea0933 1 b3f3d078be3e40359336ad956a3c4157 24315944e1fb439fa1bc0c06fbeaa548--b3f3d078be3e40359336ad956a3c4157 fcd46bbfc0ea407aa595870512b0d7ac 081f00ae96fa4632922e74213b4e47ce Y 036b0eb57fea4304b084614164ea0933--081f00ae96fa4632922e74213b4e47ce 081f00ae96fa4632922e74213b4e47ce--fcd46bbfc0ea407aa595870512b0d7ac

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 e2a1068a1b4b4d23a6717f13d69ee8c1 0 e8cecf9c095b42a29dc29c132a17a3cf X e2a1068a1b4b4d23a6717f13d69ee8c1--e8cecf9c095b42a29dc29c132a17a3cf 9122ec07021842dcb9f2ac3c2c8c61ef 1 e42ab7a36e4e4b108a82970030a08bb5 Y e8cecf9c095b42a29dc29c132a17a3cf--e42ab7a36e4e4b108a82970030a08bb5 75485875b90043b8905939538dc525c5 e42ab7a36e4e4b108a82970030a08bb5--75485875b90043b8905939538dc525c5 07d13249d37342438d6c7f940c9d24a5 cf76292b4edd4d6487a9ea0b785a078a X 9122ec07021842dcb9f2ac3c2c8c61ef--cf76292b4edd4d6487a9ea0b785a078a 27bd249ca0b24642ac5199241fc00356 Y cf76292b4edd4d6487a9ea0b785a078a--27bd249ca0b24642ac5199241fc00356 27bd249ca0b24642ac5199241fc00356--07d13249d37342438d6c7f940c9d24a5

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 b3d76e8660c640118268b01b60a057e7 0 cd702b363a9c485f90d86a025993cf5c H b3d76e8660c640118268b01b60a057e7--cd702b363a9c485f90d86a025993cf5c 9a743f08f7034c9a96ee20bca9866cf8 1 5828d5d43f214f86a1875902e450123e PHASE(1.571) cd702b363a9c485f90d86a025993cf5c--5828d5d43f214f86a1875902e450123e 23763f0702894f619894772751871d9f PHASE(0.785) 5828d5d43f214f86a1875902e450123e--23763f0702894f619894772751871d9f a83509fbbd314c9d91ba71cf1528215b 5828d5d43f214f86a1875902e450123e--a83509fbbd314c9d91ba71cf1528215b 3f4a11568cfd481cae017dacdbfe27ac 23763f0702894f619894772751871d9f--3f4a11568cfd481cae017dacdbfe27ac b108570bc06f45e8b58355b0425a8781 23763f0702894f619894772751871d9f--b108570bc06f45e8b58355b0425a8781 70d4f176c57647669b2606f0d6aef390 3f4a11568cfd481cae017dacdbfe27ac--70d4f176c57647669b2606f0d6aef390 2099aa389fa941e6a5fd852f532ee827 70d4f176c57647669b2606f0d6aef390--2099aa389fa941e6a5fd852f532ee827 c236f439107c4e85be8b4beda4b73c02 2099aa389fa941e6a5fd852f532ee827--c236f439107c4e85be8b4beda4b73c02 2277139f2a974359a0fc6c8e0ae501e3 d932d6e9569f421cb308b08156d38eed 9a743f08f7034c9a96ee20bca9866cf8--d932d6e9569f421cb308b08156d38eed cc413f80beb84868a781c1a596c40e60 2 d932d6e9569f421cb308b08156d38eed--a83509fbbd314c9d91ba71cf1528215b 1640ba18b0cd44ce9cccc1ebccf7c27b a83509fbbd314c9d91ba71cf1528215b--1640ba18b0cd44ce9cccc1ebccf7c27b ad2089beaa0743cd8a0cec3dfe7b1a42 H 1640ba18b0cd44ce9cccc1ebccf7c27b--ad2089beaa0743cd8a0cec3dfe7b1a42 3b7e7df5a64e44eaa8ce5da44f32f899 PHASE(1.571) ad2089beaa0743cd8a0cec3dfe7b1a42--3b7e7df5a64e44eaa8ce5da44f32f899 6c4e347592604ec5b6e56fce939daf55 3b7e7df5a64e44eaa8ce5da44f32f899--6c4e347592604ec5b6e56fce939daf55 7d6b00eaa2e643f3b1679c6898f5f74a 3b7e7df5a64e44eaa8ce5da44f32f899--7d6b00eaa2e643f3b1679c6898f5f74a 6c4e347592604ec5b6e56fce939daf55--2277139f2a974359a0fc6c8e0ae501e3 4cadad1251ae4c078d43c0cf4fba081a e665c4a23ff64426a95f597f51befe28 cc413f80beb84868a781c1a596c40e60--e665c4a23ff64426a95f597f51befe28 fd3b842f46d44c878e5b30eb51a37b17 e665c4a23ff64426a95f597f51befe28--fd3b842f46d44c878e5b30eb51a37b17 fd3b842f46d44c878e5b30eb51a37b17--b108570bc06f45e8b58355b0425a8781 81ffda3a5c38412a8bb5576efa7b73b3 b108570bc06f45e8b58355b0425a8781--81ffda3a5c38412a8bb5576efa7b73b3 81ffda3a5c38412a8bb5576efa7b73b3--7d6b00eaa2e643f3b1679c6898f5f74a d33aad5707f0464a9243c4c14b33f38e H 7d6b00eaa2e643f3b1679c6898f5f74a--d33aad5707f0464a9243c4c14b33f38e d33aad5707f0464a9243c4c14b33f38e--4cadad1251ae4c078d43c0cf4fba081a

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 a0c605a190b74601bfdaa699e1706aa9 0 7ad9a09a86de407cb063d674acd709e2 a0c605a190b74601bfdaa699e1706aa9--7ad9a09a86de407cb063d674acd709e2 1f451a556f374245a95d1cbbb697dd89 1 6bead1c639d9400e9a69e98ff9f8d0c3 7ad9a09a86de407cb063d674acd709e2--6bead1c639d9400e9a69e98ff9f8d0c3 2ab69f8037274d18aca65e9384bb4921 6bead1c639d9400e9a69e98ff9f8d0c3--2ab69f8037274d18aca65e9384bb4921 6da0d101b05d428f8550a6673a54c2d4 PHASE(-0.785) 2ab69f8037274d18aca65e9384bb4921--6da0d101b05d428f8550a6673a54c2d4 812b4f9a7b8141c2855e56d1fa8baaa2 PHASE(-1.571) 6da0d101b05d428f8550a6673a54c2d4--812b4f9a7b8141c2855e56d1fa8baaa2 560d03c6424946a785b79d351a4f262b 6da0d101b05d428f8550a6673a54c2d4--560d03c6424946a785b79d351a4f262b 28dea6c91d61446b87afbd0c7d37f086 H 812b4f9a7b8141c2855e56d1fa8baaa2--28dea6c91d61446b87afbd0c7d37f086 765d5b4a84dc4c2582f7ec8318cb1a29 812b4f9a7b8141c2855e56d1fa8baaa2--765d5b4a84dc4c2582f7ec8318cb1a29 0b87852d5c6540b7a73118df3e57f044 28dea6c91d61446b87afbd0c7d37f086--0b87852d5c6540b7a73118df3e57f044 79a0e13f61434f6bab6b266a230ae86d 7010767f45214eff852c4c0baeeb3cbe 1f451a556f374245a95d1cbbb697dd89--7010767f45214eff852c4c0baeeb3cbe ea198715a3f2487cbddd142b6a0a7f9d 2 b442b9f2ec404f5082ce64adcc4cbb36 PHASE(-1.571) 7010767f45214eff852c4c0baeeb3cbe--b442b9f2ec404f5082ce64adcc4cbb36 b48337178307438e8542aaea0a6d8f24 H b442b9f2ec404f5082ce64adcc4cbb36--b48337178307438e8542aaea0a6d8f24 38d20fce67dc40408f50007640bcc862 b442b9f2ec404f5082ce64adcc4cbb36--38d20fce67dc40408f50007640bcc862 90a069a8b3074c30af5cd4705f135f89 b48337178307438e8542aaea0a6d8f24--90a069a8b3074c30af5cd4705f135f89 90a069a8b3074c30af5cd4705f135f89--765d5b4a84dc4c2582f7ec8318cb1a29 60aabb11e18d4f87a78b686772f36ca7 765d5b4a84dc4c2582f7ec8318cb1a29--60aabb11e18d4f87a78b686772f36ca7 60aabb11e18d4f87a78b686772f36ca7--79a0e13f61434f6bab6b266a230ae86d f63c1eabfde14b369c2ed6204ff5babb a6a44e15cf5b4bb9b9e8be51b227a2f1 H ea198715a3f2487cbddd142b6a0a7f9d--a6a44e15cf5b4bb9b9e8be51b227a2f1 a6a44e15cf5b4bb9b9e8be51b227a2f1--38d20fce67dc40408f50007640bcc862 37b9f5d3a1fe4e02bddeca6a9a716cfe 38d20fce67dc40408f50007640bcc862--37b9f5d3a1fe4e02bddeca6a9a716cfe 37b9f5d3a1fe4e02bddeca6a9a716cfe--560d03c6424946a785b79d351a4f262b a68f8d794d884766a96d30a1d7f67bd6 560d03c6424946a785b79d351a4f262b--a68f8d794d884766a96d30a1d7f67bd6 86a242accd184e948909c37775e37aeb a68f8d794d884766a96d30a1d7f67bd6--86a242accd184e948909c37775e37aeb 86a242accd184e948909c37775e37aeb--f63c1eabfde14b369c2ed6204ff5babb

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_8684e2b8fd5c4d768ca5dc1a4c4b4f0c 140a209ba9cc42aaad9e7afa96b3611b 0 b4575dcc4e464198848d6b448d533326 RX(0.0) 140a209ba9cc42aaad9e7afa96b3611b--b4575dcc4e464198848d6b448d533326 c33d76eafd7044c0b33dd33da1ff79b9 1 a59f16ea3efd4c979cd25a14dd112cb7 HamEvo b4575dcc4e464198848d6b448d533326--a59f16ea3efd4c979cd25a14dd112cb7 a92875b2269c48d7bb29fe81addfedcb RX(0.0) a59f16ea3efd4c979cd25a14dd112cb7--a92875b2269c48d7bb29fe81addfedcb f9439523d2a54450aea78a8a24c5a923 a92875b2269c48d7bb29fe81addfedcb--f9439523d2a54450aea78a8a24c5a923 946dbfde759e489f84e04e16f8e9f356 70d11a20aec145d7a19f5645b20da6f9 RX(1.571) c33d76eafd7044c0b33dd33da1ff79b9--70d11a20aec145d7a19f5645b20da6f9 9b8f283c4fa846329eee57db363855b7 2 e10d8d741eb24b979316666617aea68d t = 1.000 70d11a20aec145d7a19f5645b20da6f9--e10d8d741eb24b979316666617aea68d 4029dad6ec7942428ff082a44434b845 RX(1.571) e10d8d741eb24b979316666617aea68d--4029dad6ec7942428ff082a44434b845 4029dad6ec7942428ff082a44434b845--946dbfde759e489f84e04e16f8e9f356 b2986ada255a4cf99978b5d00006f5b2 972c3bfb033240c596c94034fc92e286 RX(3.142) 9b8f283c4fa846329eee57db363855b7--972c3bfb033240c596c94034fc92e286 2b309e1200d1452389906728586ffa1b 972c3bfb033240c596c94034fc92e286--2b309e1200d1452389906728586ffa1b 2ce712ff20a74599acc136b4280f3fd4 RX(3.142) 2b309e1200d1452389906728586ffa1b--2ce712ff20a74599acc136b4280f3fd4 2ce712ff20a74599acc136b4280f3fd4--b2986ada255a4cf99978b5d00006f5b2

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({'11': 271, '01': 249, '00': 240, '10': 240})]
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.