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 750cbd2190434fe2af75c4ae62885df7 0 72b3ef04e8f94f9c8ec275ad592475f0 RX(0.5) 750cbd2190434fe2af75c4ae62885df7--72b3ef04e8f94f9c8ec275ad592475f0 bd9280971a1044c09d3b1101505222ed 1 ee07462c296e4a15a52a0f73c3793e34 72b3ef04e8f94f9c8ec275ad592475f0--ee07462c296e4a15a52a0f73c3793e34 0c61dc062d0d4f19af3d2af1109245df ee07462c296e4a15a52a0f73c3793e34--0c61dc062d0d4f19af3d2af1109245df f3f74af9e5284b41964ae94823900eb6 ea08b1a780dd4b478e33c5fbb519d66e bd9280971a1044c09d3b1101505222ed--ea08b1a780dd4b478e33c5fbb519d66e ab91fb5bb28b4a07a1a53d3c95d4081e X ea08b1a780dd4b478e33c5fbb519d66e--ab91fb5bb28b4a07a1a53d3c95d4081e ab91fb5bb28b4a07a1a53d3c95d4081e--ee07462c296e4a15a52a0f73c3793e34 ab91fb5bb28b4a07a1a53d3c95d4081e--f3f74af9e5284b41964ae94823900eb6

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 9039fb74b6d44e1ba70a3b3bf93b091d 0 2e0cc03e860b4ed3ba7b4cb37e205f5b X 9039fb74b6d44e1ba70a3b3bf93b091d--2e0cc03e860b4ed3ba7b4cb37e205f5b 8d626198d87141ea8f3415ed51826d38 1 f6f06155a3aa4aee8ad48a8ea4f398ec 2e0cc03e860b4ed3ba7b4cb37e205f5b--f6f06155a3aa4aee8ad48a8ea4f398ec 63c9eedbafa1431f8bf658f1ffda18bb ffc18c60e1d547c394321264f10bb270 Y 8d626198d87141ea8f3415ed51826d38--ffc18c60e1d547c394321264f10bb270 ffc18c60e1d547c394321264f10bb270--63c9eedbafa1431f8bf658f1ffda18bb

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 6ed1e7cc8b964df492c31a434746997e 0 77e548178a8e49efb33001ac281e5b4f X 6ed1e7cc8b964df492c31a434746997e--77e548178a8e49efb33001ac281e5b4f ee2f454fb26b48e2be887ef5c91bb2aa 1 f3a1bd8f09114ab89cf1db1c39e508e8 Y 77e548178a8e49efb33001ac281e5b4f--f3a1bd8f09114ab89cf1db1c39e508e8 3071b925a8f54c78a15e01641f50433f f3a1bd8f09114ab89cf1db1c39e508e8--3071b925a8f54c78a15e01641f50433f 62fd4a24ab7d4115930c4eca050e6a4a b82ee10ab56e48c4a56571846a14e053 X ee2f454fb26b48e2be887ef5c91bb2aa--b82ee10ab56e48c4a56571846a14e053 8b302cda79fe4aeb85bfef65f3250f19 Y b82ee10ab56e48c4a56571846a14e053--8b302cda79fe4aeb85bfef65f3250f19 8b302cda79fe4aeb85bfef65f3250f19--62fd4a24ab7d4115930c4eca050e6a4a

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 1083dd03175a4c8182c3ef94de136f80 0 d537ce48d562486abbb1be24bcc4eba6 H 1083dd03175a4c8182c3ef94de136f80--d537ce48d562486abbb1be24bcc4eba6 cc33f9797d144c13ac9f239a3c1839d3 1 d1830ec2bcf447f3b582ada628c42780 PHASE(1.571) d537ce48d562486abbb1be24bcc4eba6--d1830ec2bcf447f3b582ada628c42780 7a95f91aa80442a9a921b9d2d522b450 PHASE(0.785) d1830ec2bcf447f3b582ada628c42780--7a95f91aa80442a9a921b9d2d522b450 14cb0140262547468fe4189907027aa7 d1830ec2bcf447f3b582ada628c42780--14cb0140262547468fe4189907027aa7 ad482e320f6044168f98d8e4d68e32f7 7a95f91aa80442a9a921b9d2d522b450--ad482e320f6044168f98d8e4d68e32f7 4fd1dbc5d6a8480187c812c75133b242 7a95f91aa80442a9a921b9d2d522b450--4fd1dbc5d6a8480187c812c75133b242 1867899b359743f184af2a17b4eced96 ad482e320f6044168f98d8e4d68e32f7--1867899b359743f184af2a17b4eced96 384ebdd9bdaf4d64b81f55e84182ae92 1867899b359743f184af2a17b4eced96--384ebdd9bdaf4d64b81f55e84182ae92 eab66b7490194521ace6415f60aaf503 384ebdd9bdaf4d64b81f55e84182ae92--eab66b7490194521ace6415f60aaf503 a186e4e388f941a19f6946c4ad02012c 6ab20a3a2951462aab198d9b84504d93 cc33f9797d144c13ac9f239a3c1839d3--6ab20a3a2951462aab198d9b84504d93 b33b4be1070f4b52bf5c83e20c0d3b8a 2 6ab20a3a2951462aab198d9b84504d93--14cb0140262547468fe4189907027aa7 800249c1b8864e7eb61d35a335e15d24 14cb0140262547468fe4189907027aa7--800249c1b8864e7eb61d35a335e15d24 57d1764502ec4dc5822db536e84e0eb5 H 800249c1b8864e7eb61d35a335e15d24--57d1764502ec4dc5822db536e84e0eb5 18a7ff172e5c49f7a73192a2e9789b98 PHASE(1.571) 57d1764502ec4dc5822db536e84e0eb5--18a7ff172e5c49f7a73192a2e9789b98 a7da57f6b7d24c5da6f2dfef28f603dc 18a7ff172e5c49f7a73192a2e9789b98--a7da57f6b7d24c5da6f2dfef28f603dc 0ee35f5ae0a04b1f887e1dc26705c0b1 18a7ff172e5c49f7a73192a2e9789b98--0ee35f5ae0a04b1f887e1dc26705c0b1 a7da57f6b7d24c5da6f2dfef28f603dc--a186e4e388f941a19f6946c4ad02012c 388842dc0f7e497f85fa5158d50815c0 38bfccaf54b64bf9b234877adfbb3f42 b33b4be1070f4b52bf5c83e20c0d3b8a--38bfccaf54b64bf9b234877adfbb3f42 70ff53771b4b4e9c96a01443353cf858 38bfccaf54b64bf9b234877adfbb3f42--70ff53771b4b4e9c96a01443353cf858 70ff53771b4b4e9c96a01443353cf858--4fd1dbc5d6a8480187c812c75133b242 3a5446f0e7d5447a8ec163ee0be9b4a7 4fd1dbc5d6a8480187c812c75133b242--3a5446f0e7d5447a8ec163ee0be9b4a7 3a5446f0e7d5447a8ec163ee0be9b4a7--0ee35f5ae0a04b1f887e1dc26705c0b1 d45814c8aa6641ccac2a3e03303f0965 H 0ee35f5ae0a04b1f887e1dc26705c0b1--d45814c8aa6641ccac2a3e03303f0965 d45814c8aa6641ccac2a3e03303f0965--388842dc0f7e497f85fa5158d50815c0

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 2f883368c0404822bef0a70a7cc59fab 0 7422c671d6eb440292e22fee79e031ae 2f883368c0404822bef0a70a7cc59fab--7422c671d6eb440292e22fee79e031ae 30d1b6f05b654cdd86e2fe66e48f6355 1 fbf55e412e784c9ab3dc88974d26bfb1 7422c671d6eb440292e22fee79e031ae--fbf55e412e784c9ab3dc88974d26bfb1 f397f0e2bb5743fdb0596657ad8db790 fbf55e412e784c9ab3dc88974d26bfb1--f397f0e2bb5743fdb0596657ad8db790 d0b81a95c157492b99cfc21ce97eb2e0 PHASE(-0.785) f397f0e2bb5743fdb0596657ad8db790--d0b81a95c157492b99cfc21ce97eb2e0 82436409071d45d2aa0b4c85a81a23d3 PHASE(-1.571) d0b81a95c157492b99cfc21ce97eb2e0--82436409071d45d2aa0b4c85a81a23d3 b9fdb2430f1642b496afdfceaba70d6f d0b81a95c157492b99cfc21ce97eb2e0--b9fdb2430f1642b496afdfceaba70d6f ac2f8f24e65c42e7b4ff8516a880323f H 82436409071d45d2aa0b4c85a81a23d3--ac2f8f24e65c42e7b4ff8516a880323f 0d98fe4a6ff646949342b0fbdb645c54 82436409071d45d2aa0b4c85a81a23d3--0d98fe4a6ff646949342b0fbdb645c54 37c7c38bff9f46ce89305fc51645a5be ac2f8f24e65c42e7b4ff8516a880323f--37c7c38bff9f46ce89305fc51645a5be acb74a84c56f4ad982d3a8ce3e082421 6c784effa7324daf861ae2bb7d6b9af4 30d1b6f05b654cdd86e2fe66e48f6355--6c784effa7324daf861ae2bb7d6b9af4 c3936fcbb33543b58cfd2b0ab22a05d7 2 4c05c2add9db489b801e30690a279b81 PHASE(-1.571) 6c784effa7324daf861ae2bb7d6b9af4--4c05c2add9db489b801e30690a279b81 06a8bf52150c4d01ac7b1e88ea56a036 H 4c05c2add9db489b801e30690a279b81--06a8bf52150c4d01ac7b1e88ea56a036 21f8b5cf43004e9fbfbfc4afcc03d7e8 4c05c2add9db489b801e30690a279b81--21f8b5cf43004e9fbfbfc4afcc03d7e8 681c1a88f934402db1bcbfb8ef3e801d 06a8bf52150c4d01ac7b1e88ea56a036--681c1a88f934402db1bcbfb8ef3e801d 681c1a88f934402db1bcbfb8ef3e801d--0d98fe4a6ff646949342b0fbdb645c54 62557ba47fbc45568cdc73c68f56b486 0d98fe4a6ff646949342b0fbdb645c54--62557ba47fbc45568cdc73c68f56b486 62557ba47fbc45568cdc73c68f56b486--acb74a84c56f4ad982d3a8ce3e082421 6bb8746457e34a4a98617c1934389ef6 62ef0ad817a2426f91010971cc76d756 H c3936fcbb33543b58cfd2b0ab22a05d7--62ef0ad817a2426f91010971cc76d756 62ef0ad817a2426f91010971cc76d756--21f8b5cf43004e9fbfbfc4afcc03d7e8 8711d09b8af34ea98a786027213551d4 21f8b5cf43004e9fbfbfc4afcc03d7e8--8711d09b8af34ea98a786027213551d4 8711d09b8af34ea98a786027213551d4--b9fdb2430f1642b496afdfceaba70d6f 7f82584aed4f433d9f737edaf83d1886 b9fdb2430f1642b496afdfceaba70d6f--7f82584aed4f433d9f737edaf83d1886 1c0ef0588c7643dcb9443e8f08c4bae4 7f82584aed4f433d9f737edaf83d1886--1c0ef0588c7643dcb9443e8f08c4bae4 1c0ef0588c7643dcb9443e8f08c4bae4--6bb8746457e34a4a98617c1934389ef6

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_413f0d1b8eb145218ad5900efc714284 f9fc282af90546dc94c35180cb0f8558 0 0ecca9533fe4478192d6311b12d0755d RX(0.0) f9fc282af90546dc94c35180cb0f8558--0ecca9533fe4478192d6311b12d0755d 0a8faa8616cb4c4e8d0a40f89649619e 1 7cdaad3cfea9448b8fbf8eed3b51b339 HamEvo 0ecca9533fe4478192d6311b12d0755d--7cdaad3cfea9448b8fbf8eed3b51b339 979ac8ffd99446e49eca86b3633c0551 RX(0.0) 7cdaad3cfea9448b8fbf8eed3b51b339--979ac8ffd99446e49eca86b3633c0551 2e6f950e36e34e9ea084d7ee488b5102 979ac8ffd99446e49eca86b3633c0551--2e6f950e36e34e9ea084d7ee488b5102 29c9a21004e64c85ac1734f49dd89d21 cf694d4793b0494fad9d1fc9fe02749a RX(1.571) 0a8faa8616cb4c4e8d0a40f89649619e--cf694d4793b0494fad9d1fc9fe02749a d865227148fc428e8f483c259f7c1ee3 2 6c97c1061b314c9baccfc1a69945dccb t = 1.000 cf694d4793b0494fad9d1fc9fe02749a--6c97c1061b314c9baccfc1a69945dccb d5a7fb6f0cfc4a80911e972560833731 RX(1.571) 6c97c1061b314c9baccfc1a69945dccb--d5a7fb6f0cfc4a80911e972560833731 d5a7fb6f0cfc4a80911e972560833731--29c9a21004e64c85ac1734f49dd89d21 50f007f2335e4f7480531a1da3291d98 c1782a7012154d51af8224e78a53bc83 RX(3.142) d865227148fc428e8f483c259f7c1ee3--c1782a7012154d51af8224e78a53bc83 65e23bddbf304ea288bead8982a44448 c1782a7012154d51af8224e78a53bc83--65e23bddbf304ea288bead8982a44448 d605467df3c54ecb8d0a0259b0254cb3 RX(3.142) 65e23bddbf304ea288bead8982a44448--d605467df3c54ecb8d0a0259b0254cb3 d605467df3c54ecb8d0a0259b0254cb3--50f007f2335e4f7480531a1da3291d98

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({'10': 258, '00': 254, '11': 254, '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.