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 24c00e8859944b519dc3cd83afa64c8a 0 f7fddb1a3ed64e20be138aab6cd3f5d3 RX(0.5) 24c00e8859944b519dc3cd83afa64c8a--f7fddb1a3ed64e20be138aab6cd3f5d3 a108098f56394259b131ed81bef13b9b 1 a578b59f60294113a2322b883544e704 f7fddb1a3ed64e20be138aab6cd3f5d3--a578b59f60294113a2322b883544e704 562213396c6946a5b091412893577fac a578b59f60294113a2322b883544e704--562213396c6946a5b091412893577fac 990a91a094484b8081036de4192a3d2e 6767a861eb2d49bcb5d0dfadf79c127b a108098f56394259b131ed81bef13b9b--6767a861eb2d49bcb5d0dfadf79c127b 73872184f07b4e208689abdd0fb49464 X 6767a861eb2d49bcb5d0dfadf79c127b--73872184f07b4e208689abdd0fb49464 73872184f07b4e208689abdd0fb49464--a578b59f60294113a2322b883544e704 73872184f07b4e208689abdd0fb49464--990a91a094484b8081036de4192a3d2e

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 34e19453ea544f32a687e8fd7ca6e72d 0 edd497c355e141e8b335b20aed53ec9d X 34e19453ea544f32a687e8fd7ca6e72d--edd497c355e141e8b335b20aed53ec9d 45f03e2362c940e1b81f8ec0322983b6 1 5c2678ae2a2d4dde868c07ef52d15d8e edd497c355e141e8b335b20aed53ec9d--5c2678ae2a2d4dde868c07ef52d15d8e 9527567064644e899e394139e1e18573 757ad4bfb2f24b75be1e7ad4896ec557 Y 45f03e2362c940e1b81f8ec0322983b6--757ad4bfb2f24b75be1e7ad4896ec557 757ad4bfb2f24b75be1e7ad4896ec557--9527567064644e899e394139e1e18573

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 657e7b2e70de470cba9821457e12779d 0 5cfac8ff89eb47f7bfa408a669007062 X 657e7b2e70de470cba9821457e12779d--5cfac8ff89eb47f7bfa408a669007062 8c3d346b7b2047578d806effc63ae296 1 08aeec478048482899736643d5e2ecc1 Y 5cfac8ff89eb47f7bfa408a669007062--08aeec478048482899736643d5e2ecc1 39b65d1ad0db404cae6cf3d5447e5ae9 08aeec478048482899736643d5e2ecc1--39b65d1ad0db404cae6cf3d5447e5ae9 fb28761de74f479788de53aac7e52089 72cce998aabc40f998d80356889cbf54 X 8c3d346b7b2047578d806effc63ae296--72cce998aabc40f998d80356889cbf54 a9e2b47ae043419f803e2e2899d91f53 Y 72cce998aabc40f998d80356889cbf54--a9e2b47ae043419f803e2e2899d91f53 a9e2b47ae043419f803e2e2899d91f53--fb28761de74f479788de53aac7e52089

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 4e6b7cb03acb4186bd308f020b16535a 0 58eae21fc2fe40cd96509028f253765a H 4e6b7cb03acb4186bd308f020b16535a--58eae21fc2fe40cd96509028f253765a dc3d933c171f4c53bb07461892b4aa18 1 61fb294805274a268bd3150ac1e9f5d4 PHASE(1.571) 58eae21fc2fe40cd96509028f253765a--61fb294805274a268bd3150ac1e9f5d4 a994f1a2ec464a138a2efd2e3ad12b7b PHASE(0.785) 61fb294805274a268bd3150ac1e9f5d4--a994f1a2ec464a138a2efd2e3ad12b7b a4a11439b0d34cf38945c02c984bf8fa 61fb294805274a268bd3150ac1e9f5d4--a4a11439b0d34cf38945c02c984bf8fa 3ae2b0bde1bb43f7ab3768d9fd0bf022 a994f1a2ec464a138a2efd2e3ad12b7b--3ae2b0bde1bb43f7ab3768d9fd0bf022 c51d19b1db8f4d7d9f61105a12ab7768 a994f1a2ec464a138a2efd2e3ad12b7b--c51d19b1db8f4d7d9f61105a12ab7768 758d81c38a2843a4a024698b998b9ae1 3ae2b0bde1bb43f7ab3768d9fd0bf022--758d81c38a2843a4a024698b998b9ae1 576065bb384f47a8ad62217cff636006 758d81c38a2843a4a024698b998b9ae1--576065bb384f47a8ad62217cff636006 94c41669618d47fe86c21df03787ed4c 576065bb384f47a8ad62217cff636006--94c41669618d47fe86c21df03787ed4c 507f533cf2bd440b9324de3cfbfa69a7 8c505921e7114d7c9a0fbbeda116de75 dc3d933c171f4c53bb07461892b4aa18--8c505921e7114d7c9a0fbbeda116de75 30923189ba544318bd8edf8055da31a6 2 8c505921e7114d7c9a0fbbeda116de75--a4a11439b0d34cf38945c02c984bf8fa 11bd70c3ee784ae68bf78437531eef1e a4a11439b0d34cf38945c02c984bf8fa--11bd70c3ee784ae68bf78437531eef1e 826c1234bfa7436483e8eeaceaf8c18c H 11bd70c3ee784ae68bf78437531eef1e--826c1234bfa7436483e8eeaceaf8c18c 7cb01d8008824aa7b55dfe1e41008c13 PHASE(1.571) 826c1234bfa7436483e8eeaceaf8c18c--7cb01d8008824aa7b55dfe1e41008c13 4334f86c65414dd88d49ea100987886f 7cb01d8008824aa7b55dfe1e41008c13--4334f86c65414dd88d49ea100987886f fa8e8cc698334e2f9d6367595e6d9c18 7cb01d8008824aa7b55dfe1e41008c13--fa8e8cc698334e2f9d6367595e6d9c18 4334f86c65414dd88d49ea100987886f--507f533cf2bd440b9324de3cfbfa69a7 7d2de6e1f4e24be29eeabce64420227d 83850d17bafd472cb243e3434d528093 30923189ba544318bd8edf8055da31a6--83850d17bafd472cb243e3434d528093 124e1832ac8144de9ca1297ba1348b0b 83850d17bafd472cb243e3434d528093--124e1832ac8144de9ca1297ba1348b0b 124e1832ac8144de9ca1297ba1348b0b--c51d19b1db8f4d7d9f61105a12ab7768 e3005a0f1a8f4743b199d70d50168dcf c51d19b1db8f4d7d9f61105a12ab7768--e3005a0f1a8f4743b199d70d50168dcf e3005a0f1a8f4743b199d70d50168dcf--fa8e8cc698334e2f9d6367595e6d9c18 a77bdc8295af4b4282e8fdfbd82c1d1d H fa8e8cc698334e2f9d6367595e6d9c18--a77bdc8295af4b4282e8fdfbd82c1d1d a77bdc8295af4b4282e8fdfbd82c1d1d--7d2de6e1f4e24be29eeabce64420227d

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 20eb5cd345cb4e6db99042c162fe6cbf 0 4b58507f3c6d4709a28e60a083ae73e8 20eb5cd345cb4e6db99042c162fe6cbf--4b58507f3c6d4709a28e60a083ae73e8 08ad963562f4426598eb883cd1c618e4 1 9a65a8bd963d4a38a9f88ee0ecc37728 4b58507f3c6d4709a28e60a083ae73e8--9a65a8bd963d4a38a9f88ee0ecc37728 37bf2547f92b442da08f353867773640 9a65a8bd963d4a38a9f88ee0ecc37728--37bf2547f92b442da08f353867773640 afa253d8701e4c04a86686e17fe9fb63 PHASE(-0.785) 37bf2547f92b442da08f353867773640--afa253d8701e4c04a86686e17fe9fb63 c473e880e4814bee8f9e9611d0f8ece2 PHASE(-1.571) afa253d8701e4c04a86686e17fe9fb63--c473e880e4814bee8f9e9611d0f8ece2 a9ad184986cb45b18972fe6d65294fe1 afa253d8701e4c04a86686e17fe9fb63--a9ad184986cb45b18972fe6d65294fe1 67891b40638044db9651f1a0e3be8c0b H c473e880e4814bee8f9e9611d0f8ece2--67891b40638044db9651f1a0e3be8c0b 7a40e19726394cb78116e70f94647d8f c473e880e4814bee8f9e9611d0f8ece2--7a40e19726394cb78116e70f94647d8f bcc1564c2c2c4d3a9514e9d94682a159 67891b40638044db9651f1a0e3be8c0b--bcc1564c2c2c4d3a9514e9d94682a159 7c54e5cf7893448f80ded82bf12634e8 948e891dfd9448d79dc7e31eec34810c 08ad963562f4426598eb883cd1c618e4--948e891dfd9448d79dc7e31eec34810c cb884d5d714d432d93c6916b699c8db1 2 5bb4d2731ab24c1295739a3371e2ae48 PHASE(-1.571) 948e891dfd9448d79dc7e31eec34810c--5bb4d2731ab24c1295739a3371e2ae48 4afac28346fa46eea8cc167896c8d720 H 5bb4d2731ab24c1295739a3371e2ae48--4afac28346fa46eea8cc167896c8d720 5141fec7a1e4452897b8fe160cca9a7c 5bb4d2731ab24c1295739a3371e2ae48--5141fec7a1e4452897b8fe160cca9a7c d57e4f90470545ecb7f604a150b1dcf5 4afac28346fa46eea8cc167896c8d720--d57e4f90470545ecb7f604a150b1dcf5 d57e4f90470545ecb7f604a150b1dcf5--7a40e19726394cb78116e70f94647d8f 39acb45e15054419b36940fb1e587f8e 7a40e19726394cb78116e70f94647d8f--39acb45e15054419b36940fb1e587f8e 39acb45e15054419b36940fb1e587f8e--7c54e5cf7893448f80ded82bf12634e8 2968430e107143179fd34c4586b195e3 3d0d275941224c21807333b3ea1cd2d2 H cb884d5d714d432d93c6916b699c8db1--3d0d275941224c21807333b3ea1cd2d2 3d0d275941224c21807333b3ea1cd2d2--5141fec7a1e4452897b8fe160cca9a7c 7dfa1d133ffc499fac8306242fe7b10c 5141fec7a1e4452897b8fe160cca9a7c--7dfa1d133ffc499fac8306242fe7b10c 7dfa1d133ffc499fac8306242fe7b10c--a9ad184986cb45b18972fe6d65294fe1 39c36748ed394f9e9e881b3231a1287e a9ad184986cb45b18972fe6d65294fe1--39c36748ed394f9e9e881b3231a1287e eebaab360b094dc49fcc06f358b2d968 39c36748ed394f9e9e881b3231a1287e--eebaab360b094dc49fcc06f358b2d968 eebaab360b094dc49fcc06f358b2d968--2968430e107143179fd34c4586b195e3

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_bfab9528a13c489da726e5453fe556c5 aedd2aa954d341e3a41c3099ba9f080c 0 929d7f09aa5e4729b70208c22ff390af RX(0.0) aedd2aa954d341e3a41c3099ba9f080c--929d7f09aa5e4729b70208c22ff390af 4fb15f3651ca430ab0c921a3a3ebb3d3 1 e662b58627b549f98f83a9fce88c349f HamEvo 929d7f09aa5e4729b70208c22ff390af--e662b58627b549f98f83a9fce88c349f 9977c1fa5c02442ab05c9953ea512852 RX(0.0) e662b58627b549f98f83a9fce88c349f--9977c1fa5c02442ab05c9953ea512852 d7508994eb02497fb07fac77a6301343 9977c1fa5c02442ab05c9953ea512852--d7508994eb02497fb07fac77a6301343 c0ca5e015ba8415eaf1a019e9c8f8a1e 747e79898a1f4d80815217c522f06ac2 RX(1.571) 4fb15f3651ca430ab0c921a3a3ebb3d3--747e79898a1f4d80815217c522f06ac2 d4f4f09b739d438f8332b2cd390eeb57 2 322282e8377046edbdbd5ad28608220f t = 1.000 747e79898a1f4d80815217c522f06ac2--322282e8377046edbdbd5ad28608220f 25dac6a6280245a5a311098880e858b5 RX(1.571) 322282e8377046edbdbd5ad28608220f--25dac6a6280245a5a311098880e858b5 25dac6a6280245a5a311098880e858b5--c0ca5e015ba8415eaf1a019e9c8f8a1e 07f1daeadd6d4be1ba51a5558df331ed d4eac60ee09541bbb9f0141d84cfdf74 RX(3.142) d4f4f09b739d438f8332b2cd390eeb57--d4eac60ee09541bbb9f0141d84cfdf74 9ecce9980d1341c9bb83b4257263acc7 d4eac60ee09541bbb9f0141d84cfdf74--9ecce9980d1341c9bb83b4257263acc7 73ba10cc822f4ad4aa971e160d5954d2 RX(3.142) 9ecce9980d1341c9bb83b4257263acc7--73ba10cc822f4ad4aa971e160d5954d2 73ba10cc822f4ad4aa971e160d5954d2--07f1daeadd6d4be1ba51a5558df331ed

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({'10': 266, '00': 254, '01': 241, '11': 239})]
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.