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 ca56dd3314c843da8af1d4637d2175f1 0 3b47266dde234b9e81ff58ab35f35cd9 RX(0.5) ca56dd3314c843da8af1d4637d2175f1--3b47266dde234b9e81ff58ab35f35cd9 1cfc385ef9264b3daf0f0d5b5b3d11e0 1 b07cad85e70a49f3baa08ce1d86ebef0 3b47266dde234b9e81ff58ab35f35cd9--b07cad85e70a49f3baa08ce1d86ebef0 b47f780100fa400ca457fce5f43c82b6 b07cad85e70a49f3baa08ce1d86ebef0--b47f780100fa400ca457fce5f43c82b6 d3e642651dca410fa16fbc5373af98fd 593a30055e454d7dbf94539c8d50042d 1cfc385ef9264b3daf0f0d5b5b3d11e0--593a30055e454d7dbf94539c8d50042d d9e6d9a7b7eb49efae62e9c565087404 X 593a30055e454d7dbf94539c8d50042d--d9e6d9a7b7eb49efae62e9c565087404 d9e6d9a7b7eb49efae62e9c565087404--b07cad85e70a49f3baa08ce1d86ebef0 d9e6d9a7b7eb49efae62e9c565087404--d3e642651dca410fa16fbc5373af98fd

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 0476071c86cd45ed9734dc3cc99267a2 0 ffce24a97fdd4358ae705bab03e11235 X 0476071c86cd45ed9734dc3cc99267a2--ffce24a97fdd4358ae705bab03e11235 9d7800af0584490ab355558b8dd021d9 1 caa313053c784d12a1f2bf3a87a7a1b4 ffce24a97fdd4358ae705bab03e11235--caa313053c784d12a1f2bf3a87a7a1b4 af3feafaa41e42c68db1d2ff963f38d6 b39cd3a9980e493fb4377911a7639813 Y 9d7800af0584490ab355558b8dd021d9--b39cd3a9980e493fb4377911a7639813 b39cd3a9980e493fb4377911a7639813--af3feafaa41e42c68db1d2ff963f38d6

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 03002cc0f5c84167b85fcc7d60e2eb85 0 547d6a119c274b4799d72b7bb253fc0e X 03002cc0f5c84167b85fcc7d60e2eb85--547d6a119c274b4799d72b7bb253fc0e 87c6e5311a6241ef8ee87466e150a725 1 48437c4e2ae6434f9988f8e2597896ee Y 547d6a119c274b4799d72b7bb253fc0e--48437c4e2ae6434f9988f8e2597896ee a130db4779aa4b3f818b032a6176a385 48437c4e2ae6434f9988f8e2597896ee--a130db4779aa4b3f818b032a6176a385 ae702f0233354b109bcd47b679f62679 72249066a31447c2b506b9e4ed5d01e8 X 87c6e5311a6241ef8ee87466e150a725--72249066a31447c2b506b9e4ed5d01e8 7683bf42cbc545a1909bc0baf6521b86 Y 72249066a31447c2b506b9e4ed5d01e8--7683bf42cbc545a1909bc0baf6521b86 7683bf42cbc545a1909bc0baf6521b86--ae702f0233354b109bcd47b679f62679

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 7661718bf83941ccba3692887d954d17 0 73c5bea47f1843529cb8b17b6930b90f H 7661718bf83941ccba3692887d954d17--73c5bea47f1843529cb8b17b6930b90f 9c40932d5ad5417386590e019a8bb17e 1 4185b157608f46919c9dda1922360ee3 PHASE(1.571) 73c5bea47f1843529cb8b17b6930b90f--4185b157608f46919c9dda1922360ee3 6e43d4bb871148a09b0ac1d050232727 PHASE(0.785) 4185b157608f46919c9dda1922360ee3--6e43d4bb871148a09b0ac1d050232727 ae82abc5f02c426ead3141f42b468e04 4185b157608f46919c9dda1922360ee3--ae82abc5f02c426ead3141f42b468e04 8fadd9d62bac4a858efc482e2b85e9e3 6e43d4bb871148a09b0ac1d050232727--8fadd9d62bac4a858efc482e2b85e9e3 24ec0c712c874c9cadd8ebec5f745545 6e43d4bb871148a09b0ac1d050232727--24ec0c712c874c9cadd8ebec5f745545 2955cbfe64454e629acea5bef4b588e8 8fadd9d62bac4a858efc482e2b85e9e3--2955cbfe64454e629acea5bef4b588e8 ddbaee31c2904fb9894661ff419bbeb5 2955cbfe64454e629acea5bef4b588e8--ddbaee31c2904fb9894661ff419bbeb5 20c45e959faf4f84b3aadb2888e6ff1d ddbaee31c2904fb9894661ff419bbeb5--20c45e959faf4f84b3aadb2888e6ff1d d4b4f419b20449bea98fb92c21470be4 3e809f24b1624c648367db295562b07e 9c40932d5ad5417386590e019a8bb17e--3e809f24b1624c648367db295562b07e 687d9877d9c24de89017bc6d4129e931 2 3e809f24b1624c648367db295562b07e--ae82abc5f02c426ead3141f42b468e04 3aa79b4c73284798819e9dd7a7630520 ae82abc5f02c426ead3141f42b468e04--3aa79b4c73284798819e9dd7a7630520 c9e9a666e9604c98898d2432d9732850 H 3aa79b4c73284798819e9dd7a7630520--c9e9a666e9604c98898d2432d9732850 f5338c7bce0444fa99caf02d0b7ac472 PHASE(1.571) c9e9a666e9604c98898d2432d9732850--f5338c7bce0444fa99caf02d0b7ac472 30035d553f7c4d43931ee2f3b32ff227 f5338c7bce0444fa99caf02d0b7ac472--30035d553f7c4d43931ee2f3b32ff227 b374f8a36bec4372a6ef7ed95c64d469 f5338c7bce0444fa99caf02d0b7ac472--b374f8a36bec4372a6ef7ed95c64d469 30035d553f7c4d43931ee2f3b32ff227--d4b4f419b20449bea98fb92c21470be4 8587acf6e87949a49b7b9ace2cc42ce5 38e07fec0ba6479ca996ff84abf25b3b 687d9877d9c24de89017bc6d4129e931--38e07fec0ba6479ca996ff84abf25b3b e1b059799a3f44dd99e5253be964e2fc 38e07fec0ba6479ca996ff84abf25b3b--e1b059799a3f44dd99e5253be964e2fc e1b059799a3f44dd99e5253be964e2fc--24ec0c712c874c9cadd8ebec5f745545 862703f4bede46ebbe298193c094d0f6 24ec0c712c874c9cadd8ebec5f745545--862703f4bede46ebbe298193c094d0f6 862703f4bede46ebbe298193c094d0f6--b374f8a36bec4372a6ef7ed95c64d469 df1cfe32388f46e1844271ffbc133fe0 H b374f8a36bec4372a6ef7ed95c64d469--df1cfe32388f46e1844271ffbc133fe0 df1cfe32388f46e1844271ffbc133fe0--8587acf6e87949a49b7b9ace2cc42ce5

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 4d22c1d642144fc48fcf392e41ae3e6a 0 483079fe3bab4b0894090e6b0b5afe46 4d22c1d642144fc48fcf392e41ae3e6a--483079fe3bab4b0894090e6b0b5afe46 362c4d4e0add4114b6b3bdf6eda03bcf 1 80959f566cfd4b86985c4641018e5a41 483079fe3bab4b0894090e6b0b5afe46--80959f566cfd4b86985c4641018e5a41 f1d84dddb11c4bd7972b62bb182bc36d 80959f566cfd4b86985c4641018e5a41--f1d84dddb11c4bd7972b62bb182bc36d ae9ceff8077e4ccbb2b9b4d7aadf820b PHASE(-0.785) f1d84dddb11c4bd7972b62bb182bc36d--ae9ceff8077e4ccbb2b9b4d7aadf820b 8c3e8b9c6054446d91c5070f7f2e974f PHASE(-1.571) ae9ceff8077e4ccbb2b9b4d7aadf820b--8c3e8b9c6054446d91c5070f7f2e974f f176474e4e154135bb4acf0ed5a79198 ae9ceff8077e4ccbb2b9b4d7aadf820b--f176474e4e154135bb4acf0ed5a79198 a2c3142338d34b379d5f2829ee55b4b1 H 8c3e8b9c6054446d91c5070f7f2e974f--a2c3142338d34b379d5f2829ee55b4b1 0d1b6a22a49a4dc0b0209ee6e7cab8b6 8c3e8b9c6054446d91c5070f7f2e974f--0d1b6a22a49a4dc0b0209ee6e7cab8b6 d6e11bd6c2134c0091f9a92905065932 a2c3142338d34b379d5f2829ee55b4b1--d6e11bd6c2134c0091f9a92905065932 d2e31a688f3945f99cf4262a43a1ad88 cf3859e662694f5bb9c4baf32ddbb85b 362c4d4e0add4114b6b3bdf6eda03bcf--cf3859e662694f5bb9c4baf32ddbb85b eee17b3c404a456f98445191899b107d 2 429e7dedc4754caabfc4d5516598117a PHASE(-1.571) cf3859e662694f5bb9c4baf32ddbb85b--429e7dedc4754caabfc4d5516598117a 684552d4798b48ab8b5c8bcee261bfd8 H 429e7dedc4754caabfc4d5516598117a--684552d4798b48ab8b5c8bcee261bfd8 8c99a4cbfc67491fa911e6ef6e44defe 429e7dedc4754caabfc4d5516598117a--8c99a4cbfc67491fa911e6ef6e44defe 3b557e27b61447148f9d70c594f2b451 684552d4798b48ab8b5c8bcee261bfd8--3b557e27b61447148f9d70c594f2b451 3b557e27b61447148f9d70c594f2b451--0d1b6a22a49a4dc0b0209ee6e7cab8b6 a6c608d9cbf5457490b399ae9a1c593c 0d1b6a22a49a4dc0b0209ee6e7cab8b6--a6c608d9cbf5457490b399ae9a1c593c a6c608d9cbf5457490b399ae9a1c593c--d2e31a688f3945f99cf4262a43a1ad88 4e8f90e993e142128505e37747685c47 bf0ea116db224608a0fb0e93ebe14561 H eee17b3c404a456f98445191899b107d--bf0ea116db224608a0fb0e93ebe14561 bf0ea116db224608a0fb0e93ebe14561--8c99a4cbfc67491fa911e6ef6e44defe 655b675c11a44852957aff9470bc13f5 8c99a4cbfc67491fa911e6ef6e44defe--655b675c11a44852957aff9470bc13f5 655b675c11a44852957aff9470bc13f5--f176474e4e154135bb4acf0ed5a79198 8ef8d0619df64c7bb05f0c5c8217b07f f176474e4e154135bb4acf0ed5a79198--8ef8d0619df64c7bb05f0c5c8217b07f bdb6fe1139104eb99b81b10e41ca964e 8ef8d0619df64c7bb05f0c5c8217b07f--bdb6fe1139104eb99b81b10e41ca964e bdb6fe1139104eb99b81b10e41ca964e--4e8f90e993e142128505e37747685c47

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_f0ed182c7a7e4919aa021356c06b6efd 049b4cf448ea4f96804def5b80340976 0 d63d9729d8064214a7f36c8411556de9 RX(0.0) 049b4cf448ea4f96804def5b80340976--d63d9729d8064214a7f36c8411556de9 31fed87df7ae4a3ba04bc71568712820 1 377581544bac4da7b368b5df91ad4dff HamEvo d63d9729d8064214a7f36c8411556de9--377581544bac4da7b368b5df91ad4dff cbdfb2dcc1cc4e7db82079a7aa320729 RX(0.0) 377581544bac4da7b368b5df91ad4dff--cbdfb2dcc1cc4e7db82079a7aa320729 78fdc914bc704610a2f46a1edb1be083 cbdfb2dcc1cc4e7db82079a7aa320729--78fdc914bc704610a2f46a1edb1be083 67fa55876b1b49b08811dc60072b2ce7 8c83b13f36564cb59433ad1cd5c262e4 RX(1.571) 31fed87df7ae4a3ba04bc71568712820--8c83b13f36564cb59433ad1cd5c262e4 2b05e7929cfe47e7ba80e51ee9ac8ae1 2 9a27b00da4de4cfca913f1920f5158ed t = 1.000 8c83b13f36564cb59433ad1cd5c262e4--9a27b00da4de4cfca913f1920f5158ed a15032aa443d486688825c919626c72f RX(1.571) 9a27b00da4de4cfca913f1920f5158ed--a15032aa443d486688825c919626c72f a15032aa443d486688825c919626c72f--67fa55876b1b49b08811dc60072b2ce7 345b90f49348483ca4490459d41bfd73 bc709a4647bf4d0ab05a62c39d9750e8 RX(3.142) 2b05e7929cfe47e7ba80e51ee9ac8ae1--bc709a4647bf4d0ab05a62c39d9750e8 0097b5cd0eaf4cad88f393ec29792b44 bc709a4647bf4d0ab05a62c39d9750e8--0097b5cd0eaf4cad88f393ec29792b44 942d438a91374314bbd267e96b76d4d1 RX(3.142) 0097b5cd0eaf4cad88f393ec29792b44--942d438a91374314bbd267e96b76d4d1 942d438a91374314bbd267e96b76d4d1--345b90f49348483ca4490459d41bfd73

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': 273, '01': 258, '11': 237, '00': 232})]
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.