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 4aadc06c7f724e5bb37d24b49b8da2b1 0 b1c7341433bd423baa16f8bf2ff2eb98 RX(0.5) 4aadc06c7f724e5bb37d24b49b8da2b1--b1c7341433bd423baa16f8bf2ff2eb98 e61970d8eabf40a3926b728ad1b29733 1 9fadd0ae611f4c8ba2e1428e708ec76b b1c7341433bd423baa16f8bf2ff2eb98--9fadd0ae611f4c8ba2e1428e708ec76b c39f40305c8947af9d40235f01e1e5ca 9fadd0ae611f4c8ba2e1428e708ec76b--c39f40305c8947af9d40235f01e1e5ca 652c252eb5a447a6b96ec86397b69612 c04314bb64cb47f0bc8414eafdfe5f7f e61970d8eabf40a3926b728ad1b29733--c04314bb64cb47f0bc8414eafdfe5f7f c87a421fee464b4ab9265e43ea7d9eb7 X c04314bb64cb47f0bc8414eafdfe5f7f--c87a421fee464b4ab9265e43ea7d9eb7 c87a421fee464b4ab9265e43ea7d9eb7--9fadd0ae611f4c8ba2e1428e708ec76b c87a421fee464b4ab9265e43ea7d9eb7--652c252eb5a447a6b96ec86397b69612

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 17a8bec754c041adb6df01d55542ddb1 0 f64dcec69ca84a7e88fdc0b4f2b61931 X 17a8bec754c041adb6df01d55542ddb1--f64dcec69ca84a7e88fdc0b4f2b61931 dfcef45605074b509fed783db23e1e70 1 f1c86ae538734d3993338ed8e41917ad f64dcec69ca84a7e88fdc0b4f2b61931--f1c86ae538734d3993338ed8e41917ad ca84a2bce2a341a8afd60928cf8bd09f e6ac00fb1b15461ca14368ba6cc820d7 Y dfcef45605074b509fed783db23e1e70--e6ac00fb1b15461ca14368ba6cc820d7 e6ac00fb1b15461ca14368ba6cc820d7--ca84a2bce2a341a8afd60928cf8bd09f

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 8432585fc1ed48918059b465afbb37eb 0 003872b1ea934cbdaba78aae25dc82e4 X 8432585fc1ed48918059b465afbb37eb--003872b1ea934cbdaba78aae25dc82e4 97b8adaf52ee4b468d16fb4414168d5b 1 57cc2a35ae14468e9ad6cf7dd06febd4 Y 003872b1ea934cbdaba78aae25dc82e4--57cc2a35ae14468e9ad6cf7dd06febd4 2844a2206e6645a6848e0169003710f5 57cc2a35ae14468e9ad6cf7dd06febd4--2844a2206e6645a6848e0169003710f5 74da4a9c64654ec293ea88380ae345cf 98f08bf843e749f09dc8dac49862dc5d X 97b8adaf52ee4b468d16fb4414168d5b--98f08bf843e749f09dc8dac49862dc5d 05f4a0f25df948cda03cbec2e6e86e62 Y 98f08bf843e749f09dc8dac49862dc5d--05f4a0f25df948cda03cbec2e6e86e62 05f4a0f25df948cda03cbec2e6e86e62--74da4a9c64654ec293ea88380ae345cf

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 b97f3685dbcd444ca2791e25e95d7cd9 0 1ba06adb29264b4fb14436571913b806 H b97f3685dbcd444ca2791e25e95d7cd9--1ba06adb29264b4fb14436571913b806 d99e737add8244ffb2ff210bbe6a049d 1 cf554a9a44434601be73122ce79e6a66 PHASE(1.571) 1ba06adb29264b4fb14436571913b806--cf554a9a44434601be73122ce79e6a66 73d87a58222c499a8961e147262e1a2e PHASE(0.785) cf554a9a44434601be73122ce79e6a66--73d87a58222c499a8961e147262e1a2e aec2801bdde248f0a129ca3b58f6886d cf554a9a44434601be73122ce79e6a66--aec2801bdde248f0a129ca3b58f6886d 81e136d8a9854a59950917796ef9848c 73d87a58222c499a8961e147262e1a2e--81e136d8a9854a59950917796ef9848c 4990fd52b17747e785cdd8bf8cda92bc 73d87a58222c499a8961e147262e1a2e--4990fd52b17747e785cdd8bf8cda92bc fc4512e692334f598de72af796248550 81e136d8a9854a59950917796ef9848c--fc4512e692334f598de72af796248550 eb7a58ffb50945ec8f080213677c550a fc4512e692334f598de72af796248550--eb7a58ffb50945ec8f080213677c550a 831f06bf129d4583b00d2d8222fb1935 eb7a58ffb50945ec8f080213677c550a--831f06bf129d4583b00d2d8222fb1935 15a45320c33b4040bcfae75ef0198fe4 36e274b3059f44fbb5ca4b59e9b0509e d99e737add8244ffb2ff210bbe6a049d--36e274b3059f44fbb5ca4b59e9b0509e f634387c79b2475583762ecf8a4341b0 2 36e274b3059f44fbb5ca4b59e9b0509e--aec2801bdde248f0a129ca3b58f6886d 2f9fe2fbfbdf460d94f935939ed4b0ef aec2801bdde248f0a129ca3b58f6886d--2f9fe2fbfbdf460d94f935939ed4b0ef d1e3c24e2f7b451da276b87d021691e8 H 2f9fe2fbfbdf460d94f935939ed4b0ef--d1e3c24e2f7b451da276b87d021691e8 7eae5bc0e5b04a86a8cf29d9ded5316a PHASE(1.571) d1e3c24e2f7b451da276b87d021691e8--7eae5bc0e5b04a86a8cf29d9ded5316a 67478dc56c1346ae87a5d1f69dfb0bf8 7eae5bc0e5b04a86a8cf29d9ded5316a--67478dc56c1346ae87a5d1f69dfb0bf8 9f8b23558c0347a88553dfcd4fb19fb3 7eae5bc0e5b04a86a8cf29d9ded5316a--9f8b23558c0347a88553dfcd4fb19fb3 67478dc56c1346ae87a5d1f69dfb0bf8--15a45320c33b4040bcfae75ef0198fe4 ee6c156b9900450494675c5f663d48fb 8d69b84a11e54bad8c3b5044ba0c9b80 f634387c79b2475583762ecf8a4341b0--8d69b84a11e54bad8c3b5044ba0c9b80 2f98d7868f7d4c32a71a9ef1958f265a 8d69b84a11e54bad8c3b5044ba0c9b80--2f98d7868f7d4c32a71a9ef1958f265a 2f98d7868f7d4c32a71a9ef1958f265a--4990fd52b17747e785cdd8bf8cda92bc 636ef1c39f3c491e93c80de587097125 4990fd52b17747e785cdd8bf8cda92bc--636ef1c39f3c491e93c80de587097125 636ef1c39f3c491e93c80de587097125--9f8b23558c0347a88553dfcd4fb19fb3 f43bcaf91b0041f59a0be5c01bc4da85 H 9f8b23558c0347a88553dfcd4fb19fb3--f43bcaf91b0041f59a0be5c01bc4da85 f43bcaf91b0041f59a0be5c01bc4da85--ee6c156b9900450494675c5f663d48fb

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 94d5b5021287486886bd801ad7102372 0 d28487a5083e40c38b780b6e599017f5 94d5b5021287486886bd801ad7102372--d28487a5083e40c38b780b6e599017f5 d25d191ac7a84b4fad273adaaabd7c7b 1 08f224c85fde44f0b649a5bcd72feaef d28487a5083e40c38b780b6e599017f5--08f224c85fde44f0b649a5bcd72feaef c3eebac5ae1b4be6a3c903ae0a9ab50b 08f224c85fde44f0b649a5bcd72feaef--c3eebac5ae1b4be6a3c903ae0a9ab50b 6d6aa09f59324468b24185ee8aa0e892 PHASE(-0.785) c3eebac5ae1b4be6a3c903ae0a9ab50b--6d6aa09f59324468b24185ee8aa0e892 e13bb8aa75034478a7b5a0fd97120474 PHASE(-1.571) 6d6aa09f59324468b24185ee8aa0e892--e13bb8aa75034478a7b5a0fd97120474 6a841366c4f64082ad43b69e3a6b98ec 6d6aa09f59324468b24185ee8aa0e892--6a841366c4f64082ad43b69e3a6b98ec 7a4005991c9146d99949353bbb9eb839 H e13bb8aa75034478a7b5a0fd97120474--7a4005991c9146d99949353bbb9eb839 c2ec31ccc556457e8351feb6d0ce50f4 e13bb8aa75034478a7b5a0fd97120474--c2ec31ccc556457e8351feb6d0ce50f4 291e8cfde8d0405dad813d42e7dc831f 7a4005991c9146d99949353bbb9eb839--291e8cfde8d0405dad813d42e7dc831f a0861e7e57724e55add2f4430ebad37b 88209b5f4a8546c48c2658a0fad91e35 d25d191ac7a84b4fad273adaaabd7c7b--88209b5f4a8546c48c2658a0fad91e35 698f31142f05433aa51a8991ecbe46dc 2 58d476952ad942faa2e50e923e6930fc PHASE(-1.571) 88209b5f4a8546c48c2658a0fad91e35--58d476952ad942faa2e50e923e6930fc 41b1d03f16e34d9e948a7b87274fdea6 H 58d476952ad942faa2e50e923e6930fc--41b1d03f16e34d9e948a7b87274fdea6 4527196069434edabb0443cb03d23eb3 58d476952ad942faa2e50e923e6930fc--4527196069434edabb0443cb03d23eb3 301fd696c41043c58012963591eb743f 41b1d03f16e34d9e948a7b87274fdea6--301fd696c41043c58012963591eb743f 301fd696c41043c58012963591eb743f--c2ec31ccc556457e8351feb6d0ce50f4 4484936b8acc4e179d2f20242b5e031b c2ec31ccc556457e8351feb6d0ce50f4--4484936b8acc4e179d2f20242b5e031b 4484936b8acc4e179d2f20242b5e031b--a0861e7e57724e55add2f4430ebad37b 12e25ccb7c5c4353bf40211998ba928e 33a8b223111e4baba609ac2753b4228e H 698f31142f05433aa51a8991ecbe46dc--33a8b223111e4baba609ac2753b4228e 33a8b223111e4baba609ac2753b4228e--4527196069434edabb0443cb03d23eb3 f996a6633e8d43aca80f3d9df6e28cd9 4527196069434edabb0443cb03d23eb3--f996a6633e8d43aca80f3d9df6e28cd9 f996a6633e8d43aca80f3d9df6e28cd9--6a841366c4f64082ad43b69e3a6b98ec 2f4ac79c99974f7880b6b9b0551eb3c2 6a841366c4f64082ad43b69e3a6b98ec--2f4ac79c99974f7880b6b9b0551eb3c2 abb544e1934c4e3898aafa60b1f6f73f 2f4ac79c99974f7880b6b9b0551eb3c2--abb544e1934c4e3898aafa60b1f6f73f abb544e1934c4e3898aafa60b1f6f73f--12e25ccb7c5c4353bf40211998ba928e

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_80e182fce4f14f2a9c5adf2a2ca7bfb3 8457136e37ca430f826061ab73b40991 0 ae276e34a87641c7b83cf88facc08836 RX(0.0) 8457136e37ca430f826061ab73b40991--ae276e34a87641c7b83cf88facc08836 38438d7985734a58b2a780dc2b17f416 1 9447f5909fbf44cf99e92b83a3cd92bd HamEvo ae276e34a87641c7b83cf88facc08836--9447f5909fbf44cf99e92b83a3cd92bd 906add40d53e4af6ad2a77943cb17f56 RX(0.0) 9447f5909fbf44cf99e92b83a3cd92bd--906add40d53e4af6ad2a77943cb17f56 1b1f2ba52bae43b88aba72478497e1ea 906add40d53e4af6ad2a77943cb17f56--1b1f2ba52bae43b88aba72478497e1ea 88640c4fc4f947eca87ea5edb9df9c46 713a87fe6392425d96365019ccdc7bc6 RX(1.571) 38438d7985734a58b2a780dc2b17f416--713a87fe6392425d96365019ccdc7bc6 87b586104bf141e6a466540fc6c7f1d1 2 05a90281ee8842bfa20f807c8310c327 t = 1.000 713a87fe6392425d96365019ccdc7bc6--05a90281ee8842bfa20f807c8310c327 caa1c96c88754d6cbb9fe52a4cea0143 RX(1.571) 05a90281ee8842bfa20f807c8310c327--caa1c96c88754d6cbb9fe52a4cea0143 caa1c96c88754d6cbb9fe52a4cea0143--88640c4fc4f947eca87ea5edb9df9c46 6329cfccb54e430397ebacd0bc580b05 0cb5150b02e94b408d7a320b35da2ddc RX(3.142) 87b586104bf141e6a466540fc6c7f1d1--0cb5150b02e94b408d7a320b35da2ddc 491e976efb5743299d2d18093c95c1e7 0cb5150b02e94b408d7a320b35da2ddc--491e976efb5743299d2d18093c95c1e7 3994fc54ba45439ab86b8674748a8eee RX(3.142) 491e976efb5743299d2d18093c95c1e7--3994fc54ba45439ab86b8674748a8eee 3994fc54ba45439ab86b8674748a8eee--6329cfccb54e430397ebacd0bc580b05

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, '00': 253, '01': 243, '10': 233})]
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.