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 7326b18cd2684cfda8f4bec31603b8a2 0 93d133867d754e5cb7f6d7911d652977 RX(0.5) 7326b18cd2684cfda8f4bec31603b8a2--93d133867d754e5cb7f6d7911d652977 ea1e53ef9b6b4be1abe8f211c513f380 1 9165959359bd4dadb6a774e8923e37d1 93d133867d754e5cb7f6d7911d652977--9165959359bd4dadb6a774e8923e37d1 8a38ee05db4c4495aedaf87c68be6905 9165959359bd4dadb6a774e8923e37d1--8a38ee05db4c4495aedaf87c68be6905 c4006229e773402e8be5545c614cb3af 3613ce771b0446e0a5ebcaa8f4e87589 ea1e53ef9b6b4be1abe8f211c513f380--3613ce771b0446e0a5ebcaa8f4e87589 95f9ee7470644867b5e6d872e12441b7 X 3613ce771b0446e0a5ebcaa8f4e87589--95f9ee7470644867b5e6d872e12441b7 95f9ee7470644867b5e6d872e12441b7--9165959359bd4dadb6a774e8923e37d1 95f9ee7470644867b5e6d872e12441b7--c4006229e773402e8be5545c614cb3af

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 40c7a2f742cc4b8789c2c8c41a53d399 0 3d78bed7b222431d9d4463576d8041c2 X 40c7a2f742cc4b8789c2c8c41a53d399--3d78bed7b222431d9d4463576d8041c2 7c24ed9e885c4e94a0a99f9f58d21319 1 39601ca53c914887b1024481cf0787dd 3d78bed7b222431d9d4463576d8041c2--39601ca53c914887b1024481cf0787dd c6b2b28787a14efc9efc56d4c49da83e b99a97d7f7a345ea929228381bfb5556 Y 7c24ed9e885c4e94a0a99f9f58d21319--b99a97d7f7a345ea929228381bfb5556 b99a97d7f7a345ea929228381bfb5556--c6b2b28787a14efc9efc56d4c49da83e

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 34bb1744786c49bcb72b45c0fc098d0b 0 b2dde868d222497795e590efcb1676e4 X 34bb1744786c49bcb72b45c0fc098d0b--b2dde868d222497795e590efcb1676e4 d20082a78fea47f8ba18100783b71ca3 1 638b468ed1c3495fb684fadfbddf354f Y b2dde868d222497795e590efcb1676e4--638b468ed1c3495fb684fadfbddf354f 93fb8c3aefef499496ad158b346fd842 638b468ed1c3495fb684fadfbddf354f--93fb8c3aefef499496ad158b346fd842 817264cdf772419799a9cfa077834f18 44146076b1f84effa8e8acbeb4de6e64 X d20082a78fea47f8ba18100783b71ca3--44146076b1f84effa8e8acbeb4de6e64 4b229b18d997487aa9bda76163adbbba Y 44146076b1f84effa8e8acbeb4de6e64--4b229b18d997487aa9bda76163adbbba 4b229b18d997487aa9bda76163adbbba--817264cdf772419799a9cfa077834f18

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 ca391b8ca85544ba912aa75282ac49b7 0 8c4e31bc0f5b4ffb8131a2cd26d62bbe H ca391b8ca85544ba912aa75282ac49b7--8c4e31bc0f5b4ffb8131a2cd26d62bbe aa465cbecdfa4b848bf591d6b56df674 1 7f51b1be1de646c4a96735026a0d7c2c PHASE(1.571) 8c4e31bc0f5b4ffb8131a2cd26d62bbe--7f51b1be1de646c4a96735026a0d7c2c 305127914532453eaf71799566993148 PHASE(0.785) 7f51b1be1de646c4a96735026a0d7c2c--305127914532453eaf71799566993148 dac8613ce49a436997e9b28b5a989390 7f51b1be1de646c4a96735026a0d7c2c--dac8613ce49a436997e9b28b5a989390 c7cf172bd4c149e1931d8f7e54b58ce2 305127914532453eaf71799566993148--c7cf172bd4c149e1931d8f7e54b58ce2 3955d7eac6954df7ba6a255152859a53 305127914532453eaf71799566993148--3955d7eac6954df7ba6a255152859a53 f33c9c14d4ad412d82c79f8d9fa57bfa c7cf172bd4c149e1931d8f7e54b58ce2--f33c9c14d4ad412d82c79f8d9fa57bfa e29c3245a4c6404d9d6964149cb3a428 f33c9c14d4ad412d82c79f8d9fa57bfa--e29c3245a4c6404d9d6964149cb3a428 4869e0e6edc2407890fad0ac4d3b6f70 e29c3245a4c6404d9d6964149cb3a428--4869e0e6edc2407890fad0ac4d3b6f70 af3edb6f26fa4774ad67e7466126d540 97e67d10da304117bac29e5e0ea040f2 aa465cbecdfa4b848bf591d6b56df674--97e67d10da304117bac29e5e0ea040f2 197c98b703a445f3828afa11dcc01891 2 97e67d10da304117bac29e5e0ea040f2--dac8613ce49a436997e9b28b5a989390 8d5b6d43f4494fb08ed92d25cc90a677 dac8613ce49a436997e9b28b5a989390--8d5b6d43f4494fb08ed92d25cc90a677 0816d2695eb048de8cbd10bcbf802c42 H 8d5b6d43f4494fb08ed92d25cc90a677--0816d2695eb048de8cbd10bcbf802c42 2cc3691b6a634381a111bd87737a54a5 PHASE(1.571) 0816d2695eb048de8cbd10bcbf802c42--2cc3691b6a634381a111bd87737a54a5 5fdd91f218c24864b299db41cd49250b 2cc3691b6a634381a111bd87737a54a5--5fdd91f218c24864b299db41cd49250b 925a02e6663b41aabcdbd622e8e27ece 2cc3691b6a634381a111bd87737a54a5--925a02e6663b41aabcdbd622e8e27ece 5fdd91f218c24864b299db41cd49250b--af3edb6f26fa4774ad67e7466126d540 a6afea8a73714492b7f35a7c43bd2b82 fc9110985ba74dada571651f5756d68b 197c98b703a445f3828afa11dcc01891--fc9110985ba74dada571651f5756d68b 645527422ad64e4a9931274722773e65 fc9110985ba74dada571651f5756d68b--645527422ad64e4a9931274722773e65 645527422ad64e4a9931274722773e65--3955d7eac6954df7ba6a255152859a53 2e50798570c344dd9710105e5b80c38e 3955d7eac6954df7ba6a255152859a53--2e50798570c344dd9710105e5b80c38e 2e50798570c344dd9710105e5b80c38e--925a02e6663b41aabcdbd622e8e27ece c3fe3279f04245ea92c07def798d1d7f H 925a02e6663b41aabcdbd622e8e27ece--c3fe3279f04245ea92c07def798d1d7f c3fe3279f04245ea92c07def798d1d7f--a6afea8a73714492b7f35a7c43bd2b82

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 a93523f5552b4ffeb87cd8d02315254c 0 cec29d7f13484ea490a3dfcc92ae09d2 a93523f5552b4ffeb87cd8d02315254c--cec29d7f13484ea490a3dfcc92ae09d2 28112857066542c8a6394f6f84a4d8d1 1 00b3be6c952044db9a461293688f7568 cec29d7f13484ea490a3dfcc92ae09d2--00b3be6c952044db9a461293688f7568 f824ccb5f7fb404a942d63b709388441 00b3be6c952044db9a461293688f7568--f824ccb5f7fb404a942d63b709388441 835e8af90fd94218b0828f49cd208100 PHASE(-0.785) f824ccb5f7fb404a942d63b709388441--835e8af90fd94218b0828f49cd208100 b55d07e1a8e14cdeb4ccee0636a4a13a PHASE(-1.571) 835e8af90fd94218b0828f49cd208100--b55d07e1a8e14cdeb4ccee0636a4a13a 0f69a17bdde040f88a574022c63478b9 835e8af90fd94218b0828f49cd208100--0f69a17bdde040f88a574022c63478b9 530afb450d594cc0b88afe6e2e14b1fd H b55d07e1a8e14cdeb4ccee0636a4a13a--530afb450d594cc0b88afe6e2e14b1fd aa3240b406b34eaf9d9ee30271412f54 b55d07e1a8e14cdeb4ccee0636a4a13a--aa3240b406b34eaf9d9ee30271412f54 b7709de419c3437688c84d6425f178ce 530afb450d594cc0b88afe6e2e14b1fd--b7709de419c3437688c84d6425f178ce 2ea5c9a68135449e851eaadb004e076b a434a88d4d4a4a8999c9d89200c0d439 28112857066542c8a6394f6f84a4d8d1--a434a88d4d4a4a8999c9d89200c0d439 f0eb697d71284d96bc003d9ccdb1c27b 2 a651a8f411d64685a2b5e91fe13c785d PHASE(-1.571) a434a88d4d4a4a8999c9d89200c0d439--a651a8f411d64685a2b5e91fe13c785d 9a57d8f8f0a14328b6c7006f26c5c6b2 H a651a8f411d64685a2b5e91fe13c785d--9a57d8f8f0a14328b6c7006f26c5c6b2 c035bc5fc835421ba94a36ffee04815b a651a8f411d64685a2b5e91fe13c785d--c035bc5fc835421ba94a36ffee04815b 2a046e0937d6457bb02d5fa6df4dd7f0 9a57d8f8f0a14328b6c7006f26c5c6b2--2a046e0937d6457bb02d5fa6df4dd7f0 2a046e0937d6457bb02d5fa6df4dd7f0--aa3240b406b34eaf9d9ee30271412f54 252f07f0648f4d8e802bf67dc45f14c6 aa3240b406b34eaf9d9ee30271412f54--252f07f0648f4d8e802bf67dc45f14c6 252f07f0648f4d8e802bf67dc45f14c6--2ea5c9a68135449e851eaadb004e076b d7876e1424c7472fb68c844da9011ab0 011e17836766432fa3a023699c978d3d H f0eb697d71284d96bc003d9ccdb1c27b--011e17836766432fa3a023699c978d3d 011e17836766432fa3a023699c978d3d--c035bc5fc835421ba94a36ffee04815b e33c4d9e8c5f49dd9c9734b802b3ec63 c035bc5fc835421ba94a36ffee04815b--e33c4d9e8c5f49dd9c9734b802b3ec63 e33c4d9e8c5f49dd9c9734b802b3ec63--0f69a17bdde040f88a574022c63478b9 45d6c386dfbe4ce6b91e06235b9842a1 0f69a17bdde040f88a574022c63478b9--45d6c386dfbe4ce6b91e06235b9842a1 04da6e6d619847e8bf6cf5b3432be352 45d6c386dfbe4ce6b91e06235b9842a1--04da6e6d619847e8bf6cf5b3432be352 04da6e6d619847e8bf6cf5b3432be352--d7876e1424c7472fb68c844da9011ab0

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_4f4a7a038fa64ac7902a9d75b719fe3f 74709420eb674533826c86122d428847 0 8e10a1d7a5ef4028aeb7d283591929cc RX(0.0) 74709420eb674533826c86122d428847--8e10a1d7a5ef4028aeb7d283591929cc 7aa50619ce3d459b8b8027dca3dfbb21 1 eabe877b5b714e69806a4c60785c492c HamEvo 8e10a1d7a5ef4028aeb7d283591929cc--eabe877b5b714e69806a4c60785c492c 90f870612b734620b26b655807a9e005 RX(0.0) eabe877b5b714e69806a4c60785c492c--90f870612b734620b26b655807a9e005 f20f6dce53a4470a90295865e7a4270c 90f870612b734620b26b655807a9e005--f20f6dce53a4470a90295865e7a4270c 3b6a528c3a5e4fb49dc4a1699a553c85 07efce246e854d54b4653006319d091c RX(1.571) 7aa50619ce3d459b8b8027dca3dfbb21--07efce246e854d54b4653006319d091c 9492995ff65a46dbb9fa46632c233a79 2 afd6f0fdc11549bcbc1d3191b11be931 t = 1.000 07efce246e854d54b4653006319d091c--afd6f0fdc11549bcbc1d3191b11be931 2b3bedbd26de47deac7e0b9039721d06 RX(1.571) afd6f0fdc11549bcbc1d3191b11be931--2b3bedbd26de47deac7e0b9039721d06 2b3bedbd26de47deac7e0b9039721d06--3b6a528c3a5e4fb49dc4a1699a553c85 fced84817e364b6d9b55fbeca1db19dc aa5bd483f50d41b1a6be78cb5e89c39e RX(3.142) 9492995ff65a46dbb9fa46632c233a79--aa5bd483f50d41b1a6be78cb5e89c39e 021c919c8ea2415db6872d3b96740c9c aa5bd483f50d41b1a6be78cb5e89c39e--021c919c8ea2415db6872d3b96740c9c 3445054be39749f1ab1109cffb4c5605 RX(3.142) 021c919c8ea2415db6872d3b96740c9c--3445054be39749f1ab1109cffb4c5605 3445054be39749f1ab1109cffb4c5605--fced84817e364b6d9b55fbeca1db19dc

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