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 267026bf331747e6a83f9fcf16094c74 0 6ef39e11e6814249b943ed493ea2f16d RX(0.5) 267026bf331747e6a83f9fcf16094c74--6ef39e11e6814249b943ed493ea2f16d b5ec0a36d8dd4d0aabd5d66624d62299 1 267ca3d8bc4a484d9e7e7420ce7c9685 6ef39e11e6814249b943ed493ea2f16d--267ca3d8bc4a484d9e7e7420ce7c9685 338f35abbad94412a4b0085a93afeb54 267ca3d8bc4a484d9e7e7420ce7c9685--338f35abbad94412a4b0085a93afeb54 087952f4b3af40debccb3d62226d74ba 5c00d3fe6a0d41ad97ed77bb64fb78fc b5ec0a36d8dd4d0aabd5d66624d62299--5c00d3fe6a0d41ad97ed77bb64fb78fc 19b8c4d853194152a18337c4de6b6ab0 X 5c00d3fe6a0d41ad97ed77bb64fb78fc--19b8c4d853194152a18337c4de6b6ab0 19b8c4d853194152a18337c4de6b6ab0--267ca3d8bc4a484d9e7e7420ce7c9685 19b8c4d853194152a18337c4de6b6ab0--087952f4b3af40debccb3d62226d74ba

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 61dfbb155cd043079c5f409edd2c444b 0 bef7d8806fb64d509b7233890a3d0996 X 61dfbb155cd043079c5f409edd2c444b--bef7d8806fb64d509b7233890a3d0996 5e1c51050e624cebbaa4fe0570cb772e 1 216f917a18b2445c8fe687f481780806 bef7d8806fb64d509b7233890a3d0996--216f917a18b2445c8fe687f481780806 6e41489c1d1f4b05b001d1a173a08ee7 a633019b74a143d9bc1140f2933365f6 Y 5e1c51050e624cebbaa4fe0570cb772e--a633019b74a143d9bc1140f2933365f6 a633019b74a143d9bc1140f2933365f6--6e41489c1d1f4b05b001d1a173a08ee7

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 a1cabbd943694b7abea52e53c43ab0f1 0 21f7faadbc8144898fca583b28dcb537 X a1cabbd943694b7abea52e53c43ab0f1--21f7faadbc8144898fca583b28dcb537 7d0ad5f0b31546f0b81c53d0725990bf 1 7be1783580a2494fb06981667ab43a46 Y 21f7faadbc8144898fca583b28dcb537--7be1783580a2494fb06981667ab43a46 43e3743672ae4ebdaaa0557f0e7b9f73 7be1783580a2494fb06981667ab43a46--43e3743672ae4ebdaaa0557f0e7b9f73 9630ad765cfe4590973c7c37265ec5c3 d85a086948bb4434ba542f69627449d1 X 7d0ad5f0b31546f0b81c53d0725990bf--d85a086948bb4434ba542f69627449d1 685b1a8b0ead4075a97e32f60909096c Y d85a086948bb4434ba542f69627449d1--685b1a8b0ead4075a97e32f60909096c 685b1a8b0ead4075a97e32f60909096c--9630ad765cfe4590973c7c37265ec5c3

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 8f70dec769534409b9d95df85a475316 0 4ccaab7969bf45919a6a79ad38938991 H 8f70dec769534409b9d95df85a475316--4ccaab7969bf45919a6a79ad38938991 f9299f00c98a4e8fb0851a6348f57d03 1 3bce98970f1b41c69d7614132cf2e385 PHASE(1.571) 4ccaab7969bf45919a6a79ad38938991--3bce98970f1b41c69d7614132cf2e385 3714771ef46d4c05854f5e7c7219ceba PHASE(0.785) 3bce98970f1b41c69d7614132cf2e385--3714771ef46d4c05854f5e7c7219ceba 5469fbd5d4534f5aad8ec4075168d6ae 3bce98970f1b41c69d7614132cf2e385--5469fbd5d4534f5aad8ec4075168d6ae a16a302bb25149fcb4f2c169a36ec56b 3714771ef46d4c05854f5e7c7219ceba--a16a302bb25149fcb4f2c169a36ec56b fd8510044c2a426bb5b9a59c533c93c8 3714771ef46d4c05854f5e7c7219ceba--fd8510044c2a426bb5b9a59c533c93c8 e972035d693d422fbf70bca27474d316 a16a302bb25149fcb4f2c169a36ec56b--e972035d693d422fbf70bca27474d316 1a4edf707d1f4c85bc223bcbf07ea5dd e972035d693d422fbf70bca27474d316--1a4edf707d1f4c85bc223bcbf07ea5dd e8d7eb832f3044949ea72dc6921fd712 1a4edf707d1f4c85bc223bcbf07ea5dd--e8d7eb832f3044949ea72dc6921fd712 772ce5949c5c4f55a0d6d778d7f5ddf1 9167482132854e6f8af3460f602e1076 f9299f00c98a4e8fb0851a6348f57d03--9167482132854e6f8af3460f602e1076 c9c1337e22c6430ba15ecaab160ced12 2 9167482132854e6f8af3460f602e1076--5469fbd5d4534f5aad8ec4075168d6ae d7d55fb42d73467b9ab8cd0c3ce0e958 5469fbd5d4534f5aad8ec4075168d6ae--d7d55fb42d73467b9ab8cd0c3ce0e958 f4b40fa46d3e4bb09a7efff9a50fd6d9 H d7d55fb42d73467b9ab8cd0c3ce0e958--f4b40fa46d3e4bb09a7efff9a50fd6d9 0abc301168b44924ab33ffa12d3863f8 PHASE(1.571) f4b40fa46d3e4bb09a7efff9a50fd6d9--0abc301168b44924ab33ffa12d3863f8 7ce274f4eef849289d0b909f87831532 0abc301168b44924ab33ffa12d3863f8--7ce274f4eef849289d0b909f87831532 c7c861e690ac4f7a95cba853f822b79e 0abc301168b44924ab33ffa12d3863f8--c7c861e690ac4f7a95cba853f822b79e 7ce274f4eef849289d0b909f87831532--772ce5949c5c4f55a0d6d778d7f5ddf1 0ba6a1172cb149afaa2496dc54241050 2792b66fa3fb4c7b9449227da8b1493f c9c1337e22c6430ba15ecaab160ced12--2792b66fa3fb4c7b9449227da8b1493f b7e1c2eeeb7c46d493b0aabdc27188be 2792b66fa3fb4c7b9449227da8b1493f--b7e1c2eeeb7c46d493b0aabdc27188be b7e1c2eeeb7c46d493b0aabdc27188be--fd8510044c2a426bb5b9a59c533c93c8 9acb8c4a0e0147398ac24d3343c695fe fd8510044c2a426bb5b9a59c533c93c8--9acb8c4a0e0147398ac24d3343c695fe 9acb8c4a0e0147398ac24d3343c695fe--c7c861e690ac4f7a95cba853f822b79e 0a6386ff05fa4c2d9c3f6553e71ec10b H c7c861e690ac4f7a95cba853f822b79e--0a6386ff05fa4c2d9c3f6553e71ec10b 0a6386ff05fa4c2d9c3f6553e71ec10b--0ba6a1172cb149afaa2496dc54241050

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 903383de8c1f4c4d971f504914470d16 0 1a7ed2349c25451db172a663eff0576c 903383de8c1f4c4d971f504914470d16--1a7ed2349c25451db172a663eff0576c 1431f688914d429d94d187709c82f9f1 1 f8f482f5937c457ebe0d98eef864ec13 1a7ed2349c25451db172a663eff0576c--f8f482f5937c457ebe0d98eef864ec13 a2d7685b2ceb4717a49b54f7653fe298 f8f482f5937c457ebe0d98eef864ec13--a2d7685b2ceb4717a49b54f7653fe298 a6b188d0f88f4ed38af6ac99661aea20 PHASE(-0.785) a2d7685b2ceb4717a49b54f7653fe298--a6b188d0f88f4ed38af6ac99661aea20 b1a81f466c16410b8c6e3bc76facffb4 PHASE(-1.571) a6b188d0f88f4ed38af6ac99661aea20--b1a81f466c16410b8c6e3bc76facffb4 347d370b161d450486042ee4585da806 a6b188d0f88f4ed38af6ac99661aea20--347d370b161d450486042ee4585da806 cc4e1eeb1e4c489c9f6a6b8c5973050e H b1a81f466c16410b8c6e3bc76facffb4--cc4e1eeb1e4c489c9f6a6b8c5973050e 3b05c6a63b2b45bba1417d49d34c14b6 b1a81f466c16410b8c6e3bc76facffb4--3b05c6a63b2b45bba1417d49d34c14b6 822e82a1f59d4c0daf2f23cb90b53108 cc4e1eeb1e4c489c9f6a6b8c5973050e--822e82a1f59d4c0daf2f23cb90b53108 9e12647bd10342bbadb9647092638713 317cd65248df4b40872dd97d4d458691 1431f688914d429d94d187709c82f9f1--317cd65248df4b40872dd97d4d458691 6677b0f29c274685b67b7c87aa383c78 2 c149ec5e76e0467eb3f4ad2d6a7870f0 PHASE(-1.571) 317cd65248df4b40872dd97d4d458691--c149ec5e76e0467eb3f4ad2d6a7870f0 758a12bacea1480c8e7d61a0788be239 H c149ec5e76e0467eb3f4ad2d6a7870f0--758a12bacea1480c8e7d61a0788be239 810ae052366d4b5fbfbf3006ef251998 c149ec5e76e0467eb3f4ad2d6a7870f0--810ae052366d4b5fbfbf3006ef251998 095331b2e7d84b6899cb836afc459e9e 758a12bacea1480c8e7d61a0788be239--095331b2e7d84b6899cb836afc459e9e 095331b2e7d84b6899cb836afc459e9e--3b05c6a63b2b45bba1417d49d34c14b6 891608a9de82443cafff7f804c545c7c 3b05c6a63b2b45bba1417d49d34c14b6--891608a9de82443cafff7f804c545c7c 891608a9de82443cafff7f804c545c7c--9e12647bd10342bbadb9647092638713 29a33d701f884c6c85d1ed7caa35eb25 52b03a64869843408ccb2516a44e8fd1 H 6677b0f29c274685b67b7c87aa383c78--52b03a64869843408ccb2516a44e8fd1 52b03a64869843408ccb2516a44e8fd1--810ae052366d4b5fbfbf3006ef251998 b6ceae7458ff4fc0b5b1d5f41ccbb1ff 810ae052366d4b5fbfbf3006ef251998--b6ceae7458ff4fc0b5b1d5f41ccbb1ff b6ceae7458ff4fc0b5b1d5f41ccbb1ff--347d370b161d450486042ee4585da806 d484f27626fd46d39878dfd5f66c4607 347d370b161d450486042ee4585da806--d484f27626fd46d39878dfd5f66c4607 c268b5e65ebf4c73a088e694a3e11781 d484f27626fd46d39878dfd5f66c4607--c268b5e65ebf4c73a088e694a3e11781 c268b5e65ebf4c73a088e694a3e11781--29a33d701f884c6c85d1ed7caa35eb25

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_44fb4e096ea44c7196f6ad6acefd38fe 0795d97e39a3419eab1fe2720da065bc 0 aca83fad13134654b6cd8c9c87dddd5f RX(0.0) 0795d97e39a3419eab1fe2720da065bc--aca83fad13134654b6cd8c9c87dddd5f 1bb1f63e904f4774a56e2a80d4107538 1 e4bfec4472e447a6a10d6d51f338c8b0 HamEvo aca83fad13134654b6cd8c9c87dddd5f--e4bfec4472e447a6a10d6d51f338c8b0 c2565af395bc48d9a9cdd7f3229d013f RX(0.0) e4bfec4472e447a6a10d6d51f338c8b0--c2565af395bc48d9a9cdd7f3229d013f 651faf7a7c0b4302870d04021d3a4d8c c2565af395bc48d9a9cdd7f3229d013f--651faf7a7c0b4302870d04021d3a4d8c 3688b0e97c194c6e817827cf9c091ea2 a05078df92c241a0a21a17911baad8d0 RX(1.571) 1bb1f63e904f4774a56e2a80d4107538--a05078df92c241a0a21a17911baad8d0 fbfd6261b9764bec8d8467224af35083 2 1606d8d9b1234c76b36ffe2944c658ee t = 1.000 a05078df92c241a0a21a17911baad8d0--1606d8d9b1234c76b36ffe2944c658ee fa4e495c2f3b45e794cab6d04ece108c RX(1.571) 1606d8d9b1234c76b36ffe2944c658ee--fa4e495c2f3b45e794cab6d04ece108c fa4e495c2f3b45e794cab6d04ece108c--3688b0e97c194c6e817827cf9c091ea2 583cdcd6482c44329986cc236d050ea4 bf69be9809664ac99d9143aae283fcf7 RX(3.142) fbfd6261b9764bec8d8467224af35083--bf69be9809664ac99d9143aae283fcf7 b6da51ce4a6f42b7ae19c283499f54d7 bf69be9809664ac99d9143aae283fcf7--b6da51ce4a6f42b7ae19c283499f54d7 a279956afd8844b48b34b10db632e031 RX(3.142) b6da51ce4a6f42b7ae19c283499f54d7--a279956afd8844b48b34b10db632e031 a279956afd8844b48b34b10db632e031--583cdcd6482c44329986cc236d050ea4

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': 273, '01': 266, '11': 239, '00': 222})]
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.