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 88e0e6c6ff5141e5bf2ddffdb8958a04 0 63f62f71425647858d27f9f4e28871e3 RX(0.5) 88e0e6c6ff5141e5bf2ddffdb8958a04--63f62f71425647858d27f9f4e28871e3 7c6ab787194b4ddea414879b24df2ee4 1 f5e9144505b9428b8d04963f2f28623a 63f62f71425647858d27f9f4e28871e3--f5e9144505b9428b8d04963f2f28623a d85a449e2370447ea7b48061afc5707b f5e9144505b9428b8d04963f2f28623a--d85a449e2370447ea7b48061afc5707b f823b3f4b9964a28bc17060a42626810 2f325d23f7874a6f8eddb10ff64ac07f 7c6ab787194b4ddea414879b24df2ee4--2f325d23f7874a6f8eddb10ff64ac07f 7a620fa468dd464d9d015bc76e97eb9f X 2f325d23f7874a6f8eddb10ff64ac07f--7a620fa468dd464d9d015bc76e97eb9f 7a620fa468dd464d9d015bc76e97eb9f--f5e9144505b9428b8d04963f2f28623a 7a620fa468dd464d9d015bc76e97eb9f--f823b3f4b9964a28bc17060a42626810

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 8e178e2f0ffc47a1bd1b53ac46f28623 0 3cbc078b89ce41f28d2c21c9459f5938 X 8e178e2f0ffc47a1bd1b53ac46f28623--3cbc078b89ce41f28d2c21c9459f5938 191040c105c04a168a74f1a40cf0d1c6 1 1cde5ce9edc6484cb064851bc3f62e48 3cbc078b89ce41f28d2c21c9459f5938--1cde5ce9edc6484cb064851bc3f62e48 61726ac0ede94346b62a948e5c99a7b8 cf9107ce498c4d22b601c9ddd54a3ab2 Y 191040c105c04a168a74f1a40cf0d1c6--cf9107ce498c4d22b601c9ddd54a3ab2 cf9107ce498c4d22b601c9ddd54a3ab2--61726ac0ede94346b62a948e5c99a7b8

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 a328f59c052143d6aac209cbc9a36b6d 0 3bf568ea09a9419787425371c59b0a0f X a328f59c052143d6aac209cbc9a36b6d--3bf568ea09a9419787425371c59b0a0f 7b05c4cf54e14390b72ae62c82798e40 1 fdeff3e6f5254ab8b504e63399d1da50 Y 3bf568ea09a9419787425371c59b0a0f--fdeff3e6f5254ab8b504e63399d1da50 c08cd0608eb24e258d9832b6a39bb489 fdeff3e6f5254ab8b504e63399d1da50--c08cd0608eb24e258d9832b6a39bb489 a790b37bff8f49d8a08fa7e9d4667413 3157896c4bcb4afc92fb501c1cc1df29 X 7b05c4cf54e14390b72ae62c82798e40--3157896c4bcb4afc92fb501c1cc1df29 85f5ef032c0d4e57a193d92c97c6eaeb Y 3157896c4bcb4afc92fb501c1cc1df29--85f5ef032c0d4e57a193d92c97c6eaeb 85f5ef032c0d4e57a193d92c97c6eaeb--a790b37bff8f49d8a08fa7e9d4667413

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 75f0a45847cc4f2891a38a5ce251f6a6 0 90f626c6e89e47b0bbc613e0c78767bf H 75f0a45847cc4f2891a38a5ce251f6a6--90f626c6e89e47b0bbc613e0c78767bf c53ab815d22248b4b8a7a525664cfc89 1 8b99c8109a4f4df588477f3bde5c13d6 PHASE(1.571) 90f626c6e89e47b0bbc613e0c78767bf--8b99c8109a4f4df588477f3bde5c13d6 e79be18327ae48e2b7cf1da27146eeb6 PHASE(0.785) 8b99c8109a4f4df588477f3bde5c13d6--e79be18327ae48e2b7cf1da27146eeb6 f0efb3f428c0407c81bcceb6fe4b01f6 8b99c8109a4f4df588477f3bde5c13d6--f0efb3f428c0407c81bcceb6fe4b01f6 f37e4dac16d64ca3b1484707c628bf4b e79be18327ae48e2b7cf1da27146eeb6--f37e4dac16d64ca3b1484707c628bf4b 8c6c159696fb47e393de56ad5c058261 e79be18327ae48e2b7cf1da27146eeb6--8c6c159696fb47e393de56ad5c058261 0fee64eb022e4547b6f63f603c5e190d f37e4dac16d64ca3b1484707c628bf4b--0fee64eb022e4547b6f63f603c5e190d 79c47529d0674a2fbc608c86255ebd53 0fee64eb022e4547b6f63f603c5e190d--79c47529d0674a2fbc608c86255ebd53 9b6b8a46d53a4bbc86dff51281e16c7f 79c47529d0674a2fbc608c86255ebd53--9b6b8a46d53a4bbc86dff51281e16c7f 3bed282c693b407b97c6368edc799f71 ea0d1646f7e54dee9a53447ea57b9938 c53ab815d22248b4b8a7a525664cfc89--ea0d1646f7e54dee9a53447ea57b9938 65fae62f7f9f4f8ab391f97ae489bb29 2 ea0d1646f7e54dee9a53447ea57b9938--f0efb3f428c0407c81bcceb6fe4b01f6 7b5b4467359746458def5c6bba8df885 f0efb3f428c0407c81bcceb6fe4b01f6--7b5b4467359746458def5c6bba8df885 42e52fe3df7348fdab5b3527ff52468f H 7b5b4467359746458def5c6bba8df885--42e52fe3df7348fdab5b3527ff52468f d9e8f4f96c3345149508bc01d1455c2c PHASE(1.571) 42e52fe3df7348fdab5b3527ff52468f--d9e8f4f96c3345149508bc01d1455c2c 6dbb92f0485542e3a201e9543697df81 d9e8f4f96c3345149508bc01d1455c2c--6dbb92f0485542e3a201e9543697df81 98846fce669c4efeb77fa425b9e4c4a3 d9e8f4f96c3345149508bc01d1455c2c--98846fce669c4efeb77fa425b9e4c4a3 6dbb92f0485542e3a201e9543697df81--3bed282c693b407b97c6368edc799f71 d605a1a38b79419882fc055b35f5dadc 5fee92a302d24c27819dd4e8bab26bbc 65fae62f7f9f4f8ab391f97ae489bb29--5fee92a302d24c27819dd4e8bab26bbc 123244a36bed4bcdace2ac07eb311f8b 5fee92a302d24c27819dd4e8bab26bbc--123244a36bed4bcdace2ac07eb311f8b 123244a36bed4bcdace2ac07eb311f8b--8c6c159696fb47e393de56ad5c058261 c52c9ea187ea48f1bf4743ae77160c04 8c6c159696fb47e393de56ad5c058261--c52c9ea187ea48f1bf4743ae77160c04 c52c9ea187ea48f1bf4743ae77160c04--98846fce669c4efeb77fa425b9e4c4a3 003f365415574c58a8c0d640c33ac16c H 98846fce669c4efeb77fa425b9e4c4a3--003f365415574c58a8c0d640c33ac16c 003f365415574c58a8c0d640c33ac16c--d605a1a38b79419882fc055b35f5dadc

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 f79c3bb317f646878b76b38d06f13a3f 0 a6c0cc65a8ca45049fc06f8d172e5683 f79c3bb317f646878b76b38d06f13a3f--a6c0cc65a8ca45049fc06f8d172e5683 e98747b217d3426085096091e620a3c7 1 5b563a926d3d4e9198efc731e7761b8b a6c0cc65a8ca45049fc06f8d172e5683--5b563a926d3d4e9198efc731e7761b8b 57ce2258747540288c7a9f1b39ef27ce 5b563a926d3d4e9198efc731e7761b8b--57ce2258747540288c7a9f1b39ef27ce b5dd278daca0421b98fb4b9ee44a152f PHASE(-0.785) 57ce2258747540288c7a9f1b39ef27ce--b5dd278daca0421b98fb4b9ee44a152f 775f31ca0bdf45d68821b7ac88b69a5b PHASE(-1.571) b5dd278daca0421b98fb4b9ee44a152f--775f31ca0bdf45d68821b7ac88b69a5b 88db146d5c424739ba1e9d1b52bbca13 b5dd278daca0421b98fb4b9ee44a152f--88db146d5c424739ba1e9d1b52bbca13 11af9ce274df4a308eddcdce88b44982 H 775f31ca0bdf45d68821b7ac88b69a5b--11af9ce274df4a308eddcdce88b44982 bc9ecc1f470d4a5fa8a2e29001f30ee5 775f31ca0bdf45d68821b7ac88b69a5b--bc9ecc1f470d4a5fa8a2e29001f30ee5 bdb265b524a84883a60b32f8a186b13f 11af9ce274df4a308eddcdce88b44982--bdb265b524a84883a60b32f8a186b13f 8418ba952ac64706b1dc7e042eed6e54 dad35054a1a54878a18f44b7adc3a4ac e98747b217d3426085096091e620a3c7--dad35054a1a54878a18f44b7adc3a4ac 25a70560978343e0a61e1c94d79d765c 2 74bbf6235f5940a29e99447dda8b756b PHASE(-1.571) dad35054a1a54878a18f44b7adc3a4ac--74bbf6235f5940a29e99447dda8b756b 6221803803e347399f9916702e6b2d8d H 74bbf6235f5940a29e99447dda8b756b--6221803803e347399f9916702e6b2d8d 869a05e912a649d999c13067ccef1b11 74bbf6235f5940a29e99447dda8b756b--869a05e912a649d999c13067ccef1b11 c18b3f058b434bed9cd472e30ba8c17f 6221803803e347399f9916702e6b2d8d--c18b3f058b434bed9cd472e30ba8c17f c18b3f058b434bed9cd472e30ba8c17f--bc9ecc1f470d4a5fa8a2e29001f30ee5 b0d70eb49fe5436a808fc0cd038278d9 bc9ecc1f470d4a5fa8a2e29001f30ee5--b0d70eb49fe5436a808fc0cd038278d9 b0d70eb49fe5436a808fc0cd038278d9--8418ba952ac64706b1dc7e042eed6e54 5bac817abcef4383a8b3ffc5966f97e2 467c7d21a8a548b6a241cdafee006fae H 25a70560978343e0a61e1c94d79d765c--467c7d21a8a548b6a241cdafee006fae 467c7d21a8a548b6a241cdafee006fae--869a05e912a649d999c13067ccef1b11 c78b1a7f1df44211bea268f0437a2047 869a05e912a649d999c13067ccef1b11--c78b1a7f1df44211bea268f0437a2047 c78b1a7f1df44211bea268f0437a2047--88db146d5c424739ba1e9d1b52bbca13 66e2c96ef0224e25af31602771430c82 88db146d5c424739ba1e9d1b52bbca13--66e2c96ef0224e25af31602771430c82 77f4c34e7ce14dd480cab5bcedf9142e 66e2c96ef0224e25af31602771430c82--77f4c34e7ce14dd480cab5bcedf9142e 77f4c34e7ce14dd480cab5bcedf9142e--5bac817abcef4383a8b3ffc5966f97e2

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_4bc579aaf97b47db9b64de0f7b467c85 db57477088654c439c2cb587e62ffba2 0 8fbb80dc751749d6852ff472bc278374 RX(0.0) db57477088654c439c2cb587e62ffba2--8fbb80dc751749d6852ff472bc278374 c7bf86c2747446f6a64609907099b54c 1 1b7c72e06f43496db8de0e9102f9aeb2 HamEvo 8fbb80dc751749d6852ff472bc278374--1b7c72e06f43496db8de0e9102f9aeb2 333eba6df94947f28886aad542bd4232 RX(0.0) 1b7c72e06f43496db8de0e9102f9aeb2--333eba6df94947f28886aad542bd4232 114910cddb5040cd88eb9d9edc10cbd4 333eba6df94947f28886aad542bd4232--114910cddb5040cd88eb9d9edc10cbd4 6536633aa14543739270802364f491b9 980e68d3809842678415f36b4f5c8ae5 RX(1.571) c7bf86c2747446f6a64609907099b54c--980e68d3809842678415f36b4f5c8ae5 24af247fb4814c269ec06bac8811bef1 2 6a9975fe20294a4986df5538b070c4d2 t = 1.000 980e68d3809842678415f36b4f5c8ae5--6a9975fe20294a4986df5538b070c4d2 3ec8f5889d2544949874c4bd140e9679 RX(1.571) 6a9975fe20294a4986df5538b070c4d2--3ec8f5889d2544949874c4bd140e9679 3ec8f5889d2544949874c4bd140e9679--6536633aa14543739270802364f491b9 11cf4e5a421e44bda4132de21bc77934 adc3822410104bfbb097ef50316056e9 RX(3.142) 24af247fb4814c269ec06bac8811bef1--adc3822410104bfbb097ef50316056e9 eb368eb155f24b20bbf4c6d0c3148f63 adc3822410104bfbb097ef50316056e9--eb368eb155f24b20bbf4c6d0c3148f63 e0296ddd42ad4c3ca82f7a4c564ecf33 RX(3.142) eb368eb155f24b20bbf4c6d0c3148f63--e0296ddd42ad4c3ca82f7a4c564ecf33 e0296ddd42ad4c3ca82f7a4c564ecf33--11cf4e5a421e44bda4132de21bc77934

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({'01': 257, '11': 256, '10': 254, '00': 233})]
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.