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 4fb39ed81e5047cc8381dfa211d824f2 0 a0cc22ef796748638d45f67da298c98b RX(0.5) 4fb39ed81e5047cc8381dfa211d824f2--a0cc22ef796748638d45f67da298c98b 56e877628bc44195858bdff3d17e38ea 1 94ea82f9722a4dc3a6c3a71d1f9b5b95 a0cc22ef796748638d45f67da298c98b--94ea82f9722a4dc3a6c3a71d1f9b5b95 97fb7ae65ba848c1a9a99eb8493a7bde 94ea82f9722a4dc3a6c3a71d1f9b5b95--97fb7ae65ba848c1a9a99eb8493a7bde cfd798eaa51f4f54a2a7049e90f8a364 8d8e47a135bb4d77a3c7b6bf26090c2f 56e877628bc44195858bdff3d17e38ea--8d8e47a135bb4d77a3c7b6bf26090c2f a35b05700273403bb482cda0831a90cc X 8d8e47a135bb4d77a3c7b6bf26090c2f--a35b05700273403bb482cda0831a90cc a35b05700273403bb482cda0831a90cc--94ea82f9722a4dc3a6c3a71d1f9b5b95 a35b05700273403bb482cda0831a90cc--cfd798eaa51f4f54a2a7049e90f8a364

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 1b6696510dd74f54804b2a25d5aff584 0 c437e7c3702f42068e99357f5644e831 X 1b6696510dd74f54804b2a25d5aff584--c437e7c3702f42068e99357f5644e831 e96f8e0fb7864ae3803e20b5f113c161 1 cd06e6eb236c4891a7b7701d9e8e0966 c437e7c3702f42068e99357f5644e831--cd06e6eb236c4891a7b7701d9e8e0966 b77871de27f7418193679647adc9b89f 11804256a47e47499a38fafb4c0e4442 Y e96f8e0fb7864ae3803e20b5f113c161--11804256a47e47499a38fafb4c0e4442 11804256a47e47499a38fafb4c0e4442--b77871de27f7418193679647adc9b89f

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 07a8a5fb636b4cf982b74d80f7b150f4 0 a9ba2d47e8ab49c8aeacca535b26fa51 X 07a8a5fb636b4cf982b74d80f7b150f4--a9ba2d47e8ab49c8aeacca535b26fa51 d50933627b5a417bab829831d8e121ae 1 a50048ac88444c00919544c3145c37e1 Y a9ba2d47e8ab49c8aeacca535b26fa51--a50048ac88444c00919544c3145c37e1 6920ad085b934da49e4daf937ddacd76 a50048ac88444c00919544c3145c37e1--6920ad085b934da49e4daf937ddacd76 fefcbbcc6f2d4e7dbda514bcbcb351cd 076ca0871bd74ace942be8e496e7c453 X d50933627b5a417bab829831d8e121ae--076ca0871bd74ace942be8e496e7c453 8545673b2d44478e8939eab3b172040e Y 076ca0871bd74ace942be8e496e7c453--8545673b2d44478e8939eab3b172040e 8545673b2d44478e8939eab3b172040e--fefcbbcc6f2d4e7dbda514bcbcb351cd

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 293ebbfa374b4aedacd499e9d84ac213 0 53c02dd9450e4c87856f1b10c560504c H 293ebbfa374b4aedacd499e9d84ac213--53c02dd9450e4c87856f1b10c560504c de3379a0621c4abcaaa81a7038745137 1 c39ca2c072514a5f875369f5f27f3db9 PHASE(1.571) 53c02dd9450e4c87856f1b10c560504c--c39ca2c072514a5f875369f5f27f3db9 5d108ff0968045f68325a9169b4b65c6 PHASE(0.785) c39ca2c072514a5f875369f5f27f3db9--5d108ff0968045f68325a9169b4b65c6 e9fa11ffd8c84f099a922e722a9b8950 c39ca2c072514a5f875369f5f27f3db9--e9fa11ffd8c84f099a922e722a9b8950 d47ed4f363504c8785e96ac32a0cfc6b 5d108ff0968045f68325a9169b4b65c6--d47ed4f363504c8785e96ac32a0cfc6b 1e7ef39d3aa344f5ba45cc5132588467 5d108ff0968045f68325a9169b4b65c6--1e7ef39d3aa344f5ba45cc5132588467 bd333671f5514caf99d0dc6feb0520d2 d47ed4f363504c8785e96ac32a0cfc6b--bd333671f5514caf99d0dc6feb0520d2 b9b148863a6f49d89f1ae67f3ff31f86 bd333671f5514caf99d0dc6feb0520d2--b9b148863a6f49d89f1ae67f3ff31f86 52ce2dcd347b4a4f9114a913e4ef6655 b9b148863a6f49d89f1ae67f3ff31f86--52ce2dcd347b4a4f9114a913e4ef6655 6a14985b1c6a4a4da1b3cd36a58c277c a40d90db68ac46daa384860e77bcaa55 de3379a0621c4abcaaa81a7038745137--a40d90db68ac46daa384860e77bcaa55 ee6f5e0053b04e53b800e9a41b8ee5df 2 a40d90db68ac46daa384860e77bcaa55--e9fa11ffd8c84f099a922e722a9b8950 33f8b756d6184d94b84badaedbf044cf e9fa11ffd8c84f099a922e722a9b8950--33f8b756d6184d94b84badaedbf044cf 57d8cf80d429402d9e01c9ed4baedf41 H 33f8b756d6184d94b84badaedbf044cf--57d8cf80d429402d9e01c9ed4baedf41 d0775ff13ea64a288377749dd68a04a0 PHASE(1.571) 57d8cf80d429402d9e01c9ed4baedf41--d0775ff13ea64a288377749dd68a04a0 814fc560539342b894fdc3b70bd12df0 d0775ff13ea64a288377749dd68a04a0--814fc560539342b894fdc3b70bd12df0 439dbd3772fe40b7a635c330c5d02486 d0775ff13ea64a288377749dd68a04a0--439dbd3772fe40b7a635c330c5d02486 814fc560539342b894fdc3b70bd12df0--6a14985b1c6a4a4da1b3cd36a58c277c 6c19bcc5c2394bd8a7aca9b3235a9aed dcb9ff3ec2314b9d9f6bab04450f7c6d ee6f5e0053b04e53b800e9a41b8ee5df--dcb9ff3ec2314b9d9f6bab04450f7c6d b5c08a6f53b9497bba9dbc632f03d76e dcb9ff3ec2314b9d9f6bab04450f7c6d--b5c08a6f53b9497bba9dbc632f03d76e b5c08a6f53b9497bba9dbc632f03d76e--1e7ef39d3aa344f5ba45cc5132588467 e1099b5cd4d64c3fbc9ddd38c8214a50 1e7ef39d3aa344f5ba45cc5132588467--e1099b5cd4d64c3fbc9ddd38c8214a50 e1099b5cd4d64c3fbc9ddd38c8214a50--439dbd3772fe40b7a635c330c5d02486 ff6bf0cef1034854a8676f75098b4ac1 H 439dbd3772fe40b7a635c330c5d02486--ff6bf0cef1034854a8676f75098b4ac1 ff6bf0cef1034854a8676f75098b4ac1--6c19bcc5c2394bd8a7aca9b3235a9aed

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 f00d6d4615ba41459f55c2d1ee161d7f 0 5f775717d6ec4e1ca0992bfec0d81073 f00d6d4615ba41459f55c2d1ee161d7f--5f775717d6ec4e1ca0992bfec0d81073 bc57915a4a5d4c3084805e6b178497ac 1 228dd41651ee4f75a55c4403b95d15a2 5f775717d6ec4e1ca0992bfec0d81073--228dd41651ee4f75a55c4403b95d15a2 cf0fc59c2d954773a058c8c2af1d4b0d 228dd41651ee4f75a55c4403b95d15a2--cf0fc59c2d954773a058c8c2af1d4b0d cd5047b39bb1439ab7299c72f0c329c7 PHASE(-0.785) cf0fc59c2d954773a058c8c2af1d4b0d--cd5047b39bb1439ab7299c72f0c329c7 9676d22047f340d68576d3ce755c4b1d PHASE(-1.571) cd5047b39bb1439ab7299c72f0c329c7--9676d22047f340d68576d3ce755c4b1d ee10c3329ea54019a065503631274640 cd5047b39bb1439ab7299c72f0c329c7--ee10c3329ea54019a065503631274640 3734e2eabfa84e06b75eab488ffd8cc9 H 9676d22047f340d68576d3ce755c4b1d--3734e2eabfa84e06b75eab488ffd8cc9 6cad4b10c7dc42f597e0e26341df996c 9676d22047f340d68576d3ce755c4b1d--6cad4b10c7dc42f597e0e26341df996c 768a962f0daf44faacf364d07137e8a5 3734e2eabfa84e06b75eab488ffd8cc9--768a962f0daf44faacf364d07137e8a5 33559673181e46c79890c3ed9825796e 1efc645b2f56409bad407c8f3b25bb9d bc57915a4a5d4c3084805e6b178497ac--1efc645b2f56409bad407c8f3b25bb9d 4e0a38479a16431a9982b2e559c6f12a 2 91d98a9a66644b4e838891dd13256d47 PHASE(-1.571) 1efc645b2f56409bad407c8f3b25bb9d--91d98a9a66644b4e838891dd13256d47 92458eb39d5942fdbee86cb7928c8178 H 91d98a9a66644b4e838891dd13256d47--92458eb39d5942fdbee86cb7928c8178 916635a052894369b6faa0b779ce8161 91d98a9a66644b4e838891dd13256d47--916635a052894369b6faa0b779ce8161 097bcc9ce8a9465391e860adf589e435 92458eb39d5942fdbee86cb7928c8178--097bcc9ce8a9465391e860adf589e435 097bcc9ce8a9465391e860adf589e435--6cad4b10c7dc42f597e0e26341df996c d9618c74f3ce4ea2809dc0820f1fef5b 6cad4b10c7dc42f597e0e26341df996c--d9618c74f3ce4ea2809dc0820f1fef5b d9618c74f3ce4ea2809dc0820f1fef5b--33559673181e46c79890c3ed9825796e bcf01d5ffd734fc1baf86191c1004480 9caa0fd849c7458abe8595cfc7a5ab2a H 4e0a38479a16431a9982b2e559c6f12a--9caa0fd849c7458abe8595cfc7a5ab2a 9caa0fd849c7458abe8595cfc7a5ab2a--916635a052894369b6faa0b779ce8161 b4829bf2cc5e4b08bd821e195f198796 916635a052894369b6faa0b779ce8161--b4829bf2cc5e4b08bd821e195f198796 b4829bf2cc5e4b08bd821e195f198796--ee10c3329ea54019a065503631274640 d1b771145967432bb6785db67cc32022 ee10c3329ea54019a065503631274640--d1b771145967432bb6785db67cc32022 5367738311b5468fa95d48d237730642 d1b771145967432bb6785db67cc32022--5367738311b5468fa95d48d237730642 5367738311b5468fa95d48d237730642--bcf01d5ffd734fc1baf86191c1004480

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_61d790a5badd424ea0d6dcb1af69e2f0 0b29a60b25504d8eb5ed1257d6789be4 0 dca135cd98d7464ea0a5323a52d686ee RX(0.0) 0b29a60b25504d8eb5ed1257d6789be4--dca135cd98d7464ea0a5323a52d686ee 1f1fb45fc15f4b95bacc2061a6561581 1 d9c2979a09624eb98a616ef70bb887ad HamEvo dca135cd98d7464ea0a5323a52d686ee--d9c2979a09624eb98a616ef70bb887ad aeeb3c58c72e41ad8e0e35a9f7ec780c RX(0.0) d9c2979a09624eb98a616ef70bb887ad--aeeb3c58c72e41ad8e0e35a9f7ec780c ada5cac533154f6f87977a7b535ef986 aeeb3c58c72e41ad8e0e35a9f7ec780c--ada5cac533154f6f87977a7b535ef986 478d11bd99194de38a2105f573f76562 742f172427a7400da6c3e83881b6ea47 RX(1.571) 1f1fb45fc15f4b95bacc2061a6561581--742f172427a7400da6c3e83881b6ea47 3969844de02e405fb8d6f8806251cf25 2 fadc2df22bc44d23ab7e528ba0967dc3 t = 1.000 742f172427a7400da6c3e83881b6ea47--fadc2df22bc44d23ab7e528ba0967dc3 36d80e130a0744548312289d893a5994 RX(1.571) fadc2df22bc44d23ab7e528ba0967dc3--36d80e130a0744548312289d893a5994 36d80e130a0744548312289d893a5994--478d11bd99194de38a2105f573f76562 d6df05fecebc49818c1f830173daaa88 6c26cb9bcf914967a8ef11a5aca73e2a RX(3.142) 3969844de02e405fb8d6f8806251cf25--6c26cb9bcf914967a8ef11a5aca73e2a 13bdab938130425d8fe5a2b19a5cb0c4 6c26cb9bcf914967a8ef11a5aca73e2a--13bdab938130425d8fe5a2b19a5cb0c4 fe2226c87dd0409f9abe19ecb57f03e5 RX(3.142) 13bdab938130425d8fe5a2b19a5cb0c4--fe2226c87dd0409f9abe19ecb57f03e5 fe2226c87dd0409f9abe19ecb57f03e5--d6df05fecebc49818c1f830173daaa88

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({'00': 277, '10': 246, '11': 243, '01': 234})]
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.